From df3d848a35822b9823bc87d51179521eb95d3522 Mon Sep 17 00:00:00 2001 From: Archit Rungta Date: Wed, 9 Sep 2020 15:10:19 +0530 Subject: [PATCH 01/29] Add calib3d to julia bindings --- modules/julia/CMakeLists.txt | 1 + modules/julia/README.md | 2 +- .../gen/binding_templates_cpp/cv_core.cpp | 25 ++-- modules/julia/gen/cpp_files/jlcv.hpp | 5 + modules/julia/gen/defval.txt | 36 +++++- modules/julia/gen/funclist.csv | 118 +++++++++++++++++- modules/julia/gen/gen3_julia_cxx.py | 15 --- modules/julia/gen/gen_all.py | 2 + modules/julia/gen/typemap.txt | 5 + modules/julia/samples/chessboard_corners.jl | 19 +++ modules/julia/tutorials/julia.markdown | 2 +- 11 files changed, 201 insertions(+), 29 deletions(-) create mode 100644 modules/julia/samples/chessboard_corners.jl diff --git a/modules/julia/CMakeLists.txt b/modules/julia/CMakeLists.txt index 2c47c2b5562..77158cacd7d 100644 --- a/modules/julia/CMakeLists.txt +++ b/modules/julia/CMakeLists.txt @@ -68,6 +68,7 @@ ocv_add_module( opencv_dnn opencv_features2d opencv_objdetect + opencv_calib3d ) set(HDR_PARSER_PATH ${CMAKE_SOURCE_DIR}/modules/python/src2/hdr_parser.py) diff --git a/modules/julia/README.md b/modules/julia/README.md index e19f42fd83c..f460dabdbea 100644 --- a/modules/julia/README.md +++ b/modules/julia/README.md @@ -88,4 +88,4 @@ All other types map directly to the corresponding types on C++. Unlike Python, ` Current Functionality --- -The bindings implement most of the functionality present in the core,imgproc,highgui,videoio,dnn and imgcodecs. The samples also implement some additional manually wrapped functionality. The complete list of automatically wrapped functionality is [here](gen/funclist.csv). +The bindings implement most of the functionality present in the core,imgproc,highgui,videoio,dnn,calib3d and imgcodecs. The samples also implement some additional manually wrapped functionality. The complete list of automatically wrapped functionality is [here](gen/funclist.csv). diff --git a/modules/julia/gen/binding_templates_cpp/cv_core.cpp b/modules/julia/gen/binding_templates_cpp/cv_core.cpp index 05f03f7d4ee..9966ea17747 100644 --- a/modules/julia/gen/binding_templates_cpp/cv_core.cpp +++ b/modules/julia/gen/binding_templates_cpp/cv_core.cpp @@ -41,11 +41,12 @@ typedef ParameterList> type; // #ifdef HAVE_OPENCV_FEATURES2D - template <> - struct SuperType - { - typedef cv::Algorithm type; - }; + // template <> + // struct SuperType + // { + // typedef cv::Algorithm type; + // }; + // TODO: Needs to be fixed but doesn't matter for now template <> struct SuperType { @@ -88,7 +89,6 @@ JLCXX_MODULE cv_wrap(jlcxx::Module &mod) - ${cpp_code} // // Manual Wrapping BEGIN @@ -128,16 +128,21 @@ JLCXX_MODULE cv_wrap(jlcxx::Module &mod) #endif #ifdef HAVE_OPENCV_FEATURES2D - mod.add_type("Feature2D", jlcxx::julia_base_type()); + mod.add_type("Feature2D"); mod.add_type("SimpleBlobDetector", jlcxx::julia_base_type()); mod.add_type("SimpleBlobDetector_Params"); - - mod.method("jlopencv_cv_cv_Feature2D_cv_Feature2D_detect", [](cv::Ptr &cobj, Mat &image, Mat &mask) {vector keypoints; cobj->detect(image, keypoints, mask); return keypoints; }); - mod.method("jlopencv_cv_cv_SimpleBlobDetector_create", [](SimpleBlobDetector_Params ¶meters) { auto retval = cv::SimpleBlobDetector::create(parameters); return retval; }); #endif // // Manual Wrapping END // + ${cpp_code} + +#ifdef HAVE_OPENCV_FEATURES2D + + mod.method("jlopencv_cv_cv_Feature2D_cv_Feature2D_detect", [](cv::Ptr &cobj, Mat &image, Mat &mask) {vector keypoints; cobj->detect(image, keypoints, mask); return keypoints; }); + mod.method("jlopencv_cv_cv_SimpleBlobDetector_create", [](SimpleBlobDetector_Params ¶meters) { auto retval = cv::SimpleBlobDetector::create(parameters); return retval; }); +#endif + } diff --git a/modules/julia/gen/cpp_files/jlcv.hpp b/modules/julia/gen/cpp_files/jlcv.hpp index 7fa9ea11b05..a9769c462c5 100644 --- a/modules/julia/gen/cpp_files/jlcv.hpp +++ b/modules/julia/gen/cpp_files/jlcv.hpp @@ -75,6 +75,11 @@ typedef cv::dnn::DictValue LayerId; typedef cv::dnn::Backend dnn_Backend; #endif +#ifdef HAVE_OPENCV_CALIB3D + +#include +#endif + template struct get_template_type; template diff --git a/modules/julia/gen/defval.txt b/modules/julia/gen/defval.txt index c1313a3d5fc..5d65465203a 100644 --- a/modules/julia/gen/defval.txt +++ b/modules/julia/gen/defval.txt @@ -65,4 +65,38 @@ Int32|0|0 Float64|255.|255 Scalar|Scalar(1)|cpp_to_julia(ScalarOXP()) Int32|1|1 -Size{Int32}|Size()|cpp_to_julia(SizeOP()) \ No newline at end of file +Size{Int32}|Size()|cpp_to_julia(SizeOP()) +TermCriteria|TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 20, FLT_EPSILON)|cpp_to_julia(TermCriteriaOTermCriteriaggEPSGRGTermCriteriaggCOUNTSGYWSGFLTREPSILONP()) +Int32|RANSAC|cv_RANSAC +Float32|8.0|8.0 +Float64|-1|-1 +Int32|21|21 +TermCriteria|TermCriteria( TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON)|TermCriteriaOGTermCriteriaggCOUNTGRGTermCriteriaggEPSSGZWSGDBLREPSILONP +TermCriteria|TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 1e-6)|TermCriteriaOTermCriteriaggCOUNTRTermCriteriaggEPSSGZWSGXeTcP +Int32|CALIB_CB_SYMMETRIC_GRID|cv_CALIB_CB_SYMMETRIC_GRID +InputArray|cv::Mat()|CxxMat() +Int32|SOLVEPNP_ITERATIVE|cv_SOLVEPNP_ITERATIVE +Float64|3|3 +Int32|CALIB_FIX_INTRINSIC|cv_CALIB_FIX_INTRINSIC +Float64|5|5 +Float64|0.99|0.99 +Int32|CALIB_ZERO_DISPARITY|cv_CALIB_ZERO_DISPARITY +size_t|2000|2000 +SolvePnPMethod|SOLVEPNP_ITERATIVE|cv_SOLVEPNP_ITERATIVE +Float64|0.0|0.0 +Ptr{Feature2D}|SimpleBlobDetector::create()|SimpleBlobDetectorggcreateOP +Int32|StereoSGBM::MODE_SGBM|StereoSGBMggMODERSGBM +Int32|CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE|cv_CALIB_CB_ADAPTIVE_THRESH + cv_CALIB_CB_NORMALIZE_IMAGE +Float64|3.|3 +size_t|10|10 +Int32|16|16 +Point{Float64}|Point2d(0, 0)|PointYdOWSGWP +Int32|2000|2000 +Int32|FM_RANSAC|cv_FM_RANSAC +Int32|100|100 +TermCriteria|TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 100, DBL_EPSILON)|TermCriteriaOTermCriteriaggCOUNTGRGTermCriteriaggEPSSGXWWSGDBLREPSILONP +HandEyeCalibrationMethod|CALIB_HAND_EYE_TSAI|cv_CALIB_HAND_EYE_TSAI +Float32|0.8F|0.8 +Int32|fisheye::CALIB_FIX_INTRINSIC|cv_fisheye_CALIB_FIX_INTRINSIC +Float64|0.999|0.999 +Float64|0.995|0.995 \ No newline at end of file diff --git a/modules/julia/gen/funclist.csv b/modules/julia/gen/funclist.csv index ad8a804f048..5c43e5cab23 100644 --- a/modules/julia/gen/funclist.csv +++ b/modules/julia/gen/funclist.csv @@ -373,4 +373,120 @@ cv.setTrackbarMax cv.setTrackbarMin cv.addText cv.displayOverlay -cv.displayStatusBar \ No newline at end of file +cv.displayStatusBar +cv.Rodrigues +cv.findHomography +cv.RQDecomp3x3 +cv.decomposeProjectionMatrix +cv.matMulDeriv +cv.composeRT +cv.projectPoints +cv.solvePnP +cv.solvePnPRansac +cv.solveP3P +cv.solvePnPRefineLM +cv.solvePnPRefineVVS +cv.solvePnPGeneric +cv.initCameraMatrix2D +cv.findChessboardCorners +cv.checkChessboard +cv.findChessboardCornersSB +cv.findChessboardCornersSB +cv.estimateChessboardSharpness +cv.find4QuadCornerSubpix +cv.drawChessboardCorners +cv.drawFrameAxes +cv.CirclesGridFinderParameters.CirclesGridFinderParameters +cv.findCirclesGrid +cv.findCirclesGrid +cv.calibrateCamera +cv.calibrateCamera +cv.calibrateCameraRO +cv.calibrateCameraRO +cv.calibrationMatrixValues +cv.stereoCalibrate +cv.stereoCalibrate +cv.stereoRectify +cv.stereoRectifyUncalibrated +cv.rectify3Collinear +cv.getOptimalNewCameraMatrix +cv.calibrateHandEye +cv.convertPointsToHomogeneous +cv.convertPointsFromHomogeneous +cv.findFundamentalMat +cv.findFundamentalMat +cv.findEssentialMat +cv.findEssentialMat +cv.decomposeEssentialMat +cv.recoverPose +cv.recoverPose +cv.recoverPose +cv.computeCorrespondEpilines +cv.triangulatePoints +cv.correctMatches +cv.filterSpeckles +cv.getValidDisparityROI +cv.validateDisparity +cv.reprojectImageTo3D +cv.sampsonDistance +cv.estimateAffine3D +cv.estimateTranslation3D +cv.estimateAffine2D +cv.estimateAffinePartial2D +cv.decomposeHomographyMat +cv.filterHomographyDecompByVisibleRefpoints +cv.StereoMatcher.compute +cv.StereoMatcher.getMinDisparity +cv.StereoMatcher.setMinDisparity +cv.StereoMatcher.getNumDisparities +cv.StereoMatcher.setNumDisparities +cv.StereoMatcher.getBlockSize +cv.StereoMatcher.setBlockSize +cv.StereoMatcher.getSpeckleWindowSize +cv.StereoMatcher.setSpeckleWindowSize +cv.StereoMatcher.getSpeckleRange +cv.StereoMatcher.setSpeckleRange +cv.StereoMatcher.getDisp12MaxDiff +cv.StereoMatcher.setDisp12MaxDiff +cv.StereoBM.getPreFilterType +cv.StereoBM.setPreFilterType +cv.StereoBM.getPreFilterSize +cv.StereoBM.setPreFilterSize +cv.StereoBM.getPreFilterCap +cv.StereoBM.setPreFilterCap +cv.StereoBM.getTextureThreshold +cv.StereoBM.setTextureThreshold +cv.StereoBM.getUniquenessRatio +cv.StereoBM.setUniquenessRatio +cv.StereoBM.getSmallerBlockSize +cv.StereoBM.setSmallerBlockSize +cv.StereoBM.getROI1 +cv.StereoBM.setROI1 +cv.StereoBM.getROI2 +cv.StereoBM.setROI2 +cv.StereoBM.create +cv.StereoSGBM.getPreFilterCap +cv.StereoSGBM.setPreFilterCap +cv.StereoSGBM.getUniquenessRatio +cv.StereoSGBM.setUniquenessRatio +cv.StereoSGBM.getP1 +cv.StereoSGBM.setP1 +cv.StereoSGBM.getP2 +cv.StereoSGBM.setP2 +cv.StereoSGBM.getMode +cv.StereoSGBM.setMode +cv.StereoSGBM.create +cv.undistort +cv.initUndistortRectifyMap +cv.getDefaultNewCameraMatrix +cv.undistortPoints +cv.undistortPoints +cv.fisheye.projectPoints +cv.fisheye.distortPoints +cv.fisheye.undistortPoints +cv.fisheye.initUndistortRectifyMap +cv.fisheye.undistortImage +cv.fisheye.estimateNewCameraMatrixForUndistortRectify +cv.fisheye.calibrate +cv.fisheye.stereoRectify +cv.fisheye.stereoCalibrate diff --git a/modules/julia/gen/gen3_julia_cxx.py b/modules/julia/gen/gen3_julia_cxx.py index 7dec06ab48f..73ab4eb580c 100644 --- a/modules/julia/gen/gen3_julia_cxx.py +++ b/modules/julia/gen/gen3_julia_cxx.py @@ -50,21 +50,6 @@ def handle_def_arg(inp, tp = '', ns=''): inp = inp.strip() out = '' - if tp in julia_types: - out = inp - elif not inp or inp=='Mat()': - if tp=='Mat' or tp=='InputArray': - out= 'CxxMat()' - out = tp+'()' - - elif inp=="String()": - out= '""' - - elif '(' in inp or ':' in inp: - out = "cpp_to_julia("+get_var(inp)+"())" - - else: - print("Default not found") if inp in jl_cpp_defmap[tp]: out = jl_cpp_defmap[tp][inp] diff --git a/modules/julia/gen/gen_all.py b/modules/julia/gen/gen_all.py index 701d4a91581..494c9e703be 100644 --- a/modules/julia/gen/gen_all.py +++ b/modules/julia/gen/gen_all.py @@ -32,6 +32,8 @@ hdr_list.append(mod_path+"/videoio/include/opencv2/videoio.hpp") elif module =='opencv_highgui': hdr_list.append(mod_path+"/highgui/include/opencv2/highgui.hpp") + elif module =='opencv_calib3d': + hdr_list.append(mod_path+"/calib3d/include/opencv2/calib3d.hpp") if not os.path.exists('autogen_cpp'): os.makedirs('autogen_cpp') diff --git a/modules/julia/gen/typemap.txt b/modules/julia/gen/typemap.txt index e1750ed6e90..df5e78e3270 100644 --- a/modules/julia/gen/typemap.txt +++ b/modules/julia/gen/typemap.txt @@ -45,3 +45,8 @@ vector:Array{Array{Int32, 1}, 1} float:Float32 Ptr:Ptr{Float32} vector:Array{Vec{Float32, 6}, 1} +Ptr:Ptr{Feature2D} +Point2d:Point{Float64} +SolvePnPMethod:SolvePnPMethod +CirclesGridFinderParameters:CirclesGridFinderParameters +HandEyeCalibrationMethod:HandEyeCalibrationMethod diff --git a/modules/julia/samples/chessboard_corners.jl b/modules/julia/samples/chessboard_corners.jl new file mode 100644 index 00000000000..ddd9cd5494e --- /dev/null +++ b/modules/julia/samples/chessboard_corners.jl @@ -0,0 +1,19 @@ +using OpenCV + +const cv = OpenCV + + +# chess1.png is at https://raw.githubusercontent.com/opencv/opencv_extra/master/testdata/cv/cameracalibration/chess1.png +img = cv.imread("chess1.png",cv.IMREAD_GRAYSCALE) + +# Find the chess board corners +ret, corners = cv.findChessboardCorners(img, cv.Size{Int32}(7,5)) + +# If found, add object points, image points (after refining them) +if ret + img = cv.drawChessboardCorners(img, cv.Size{Int32}(7,5), corners,ret) + cv.imshow("img",img) + cv.waitKey(Int32(0)) + + cv.destroyAllWindows() +end diff --git a/modules/julia/tutorials/julia.markdown b/modules/julia/tutorials/julia.markdown index 2e7a06031e4..f216daac4ea 100644 --- a/modules/julia/tutorials/julia.markdown +++ b/modules/julia/tutorials/julia.markdown @@ -16,7 +16,7 @@ Inspite of all this, Julia severely lacks in a lot of traditional computer visio The Bindings ----------------------- -The OpenCV bindings for Julia are created automatically using Python scripts at configure time and then installed with the Julia package manager on the system. These bindings cover most of the important functionality present in the core, imgproc, imgcodecs, highgui, videio, and dnn modules. These bindings depend on CxxWrap.jl and the process for usage and compilation is explained in detail below. The Bindings have been tested on Ubuntu and Mac. Windows might work but is not officially tested and supported right now. +The OpenCV bindings for Julia are created automatically using Python scripts at configure time and then installed with the Julia package manager on the system. These bindings cover most of the important functionality present in the core, imgproc, imgcodecs, highgui, videio, calib3d, and dnn modules. These bindings depend on CxxWrap.jl and the process for usage and compilation is explained in detail below. The Bindings have been tested on Ubuntu and Mac. Windows might work but is not officially tested and supported right now. The generation process and the method by which the binding works are similar to the Python bindings. The only major difference is that CxxWrap.jl does not support optional arguments. As a consequence, it's necessary to define the optional arguments in Julia code which adds a lot of additional complexity. From 9126355c0d25cc771cda17a956e71fe23e249f68 Mon Sep 17 00:00:00 2001 From: Paul Jurczak Date: Thu, 1 Oct 2020 04:15:34 -0600 Subject: [PATCH 02/29] Fixed a typo --- modules/rgbd/include/opencv2/rgbd/depth.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rgbd/include/opencv2/rgbd/depth.hpp b/modules/rgbd/include/opencv2/rgbd/depth.hpp index 94fdca62036..0fcd5ce7d27 100755 --- a/modules/rgbd/include/opencv2/rgbd/depth.hpp +++ b/modules/rgbd/include/opencv2/rgbd/depth.hpp @@ -564,7 +564,7 @@ namespace rgbd /** Method to compute a transformation from the source frame to the destination one. * Some odometry algorithms do not used some data of frames (eg. ICP does not use images). * In such case corresponding arguments can be set as empty Mat. - * The method returns true if all internal computions were possible (e.g. there were enough correspondences, + * The method returns true if all internal computations were possible (e.g. there were enough correspondences, * system of equations has a solution, etc) and resulting transformation satisfies some test if it's provided * by the Odometry inheritor implementation (e.g. thresholds for maximum translation and rotation). * @param srcImage Image data of the source frame (CV_8UC1) From 6f820455ac3f7ec779028489be5541516a6163a1 Mon Sep 17 00:00:00 2001 From: Vadim Pisarevsky Date: Sat, 10 Oct 2020 00:40:04 +0300 Subject: [PATCH 03/29] * catch exception when processing new variants of point cloud registration functions with UsacParams parameter; just skip this overloaded variant * added colors to chessboard_corners.jl sample --- modules/julia/gen/gen3_julia_cxx.py | 7 +++++-- modules/julia/samples/chessboard_corners.jl | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) mode change 100644 => 100755 modules/julia/gen/gen3_julia_cxx.py diff --git a/modules/julia/gen/gen3_julia_cxx.py b/modules/julia/gen/gen3_julia_cxx.py old mode 100644 new mode 100755 index 73ab4eb580c..77a2a15748d --- a/modules/julia/gen/gen3_julia_cxx.py +++ b/modules/julia/gen/gen3_julia_cxx.py @@ -114,8 +114,11 @@ def get_argument_full(self, classname='', isalgo = False): def get_argument_opt(self, ns=''): # [print(arg.default_value,":",handle_def_arg(arg.default_value, handle_jl_arg(arg.tp))) for arg in self.optlist] - str2 = ", ".join(["%s::%s = %s(%s)" % (arg.name, handle_jl_arg(arg.tp), handle_jl_arg(arg.tp) if (arg.tp == 'int' or arg.tp=='float' or arg.tp=='double') else '', handle_def_arg(arg.default_value, handle_jl_arg(arg.tp), ns)) for arg in self.optlist]) - return str2 + try: + str2 = ", ".join(["%s::%s = %s(%s)" % (arg.name, handle_jl_arg(arg.tp), handle_jl_arg(arg.tp) if (arg.tp == 'int' or arg.tp=='float' or arg.tp=='double') else '', handle_def_arg(arg.default_value, handle_jl_arg(arg.tp), ns)) for arg in self.optlist]) + return str2 + except KeyError: + return '' def get_argument_def(self, classname, isalgo): arglist = self.inlist diff --git a/modules/julia/samples/chessboard_corners.jl b/modules/julia/samples/chessboard_corners.jl index ddd9cd5494e..cc9a1e7b8dd 100644 --- a/modules/julia/samples/chessboard_corners.jl +++ b/modules/julia/samples/chessboard_corners.jl @@ -5,14 +5,15 @@ const cv = OpenCV # chess1.png is at https://raw.githubusercontent.com/opencv/opencv_extra/master/testdata/cv/cameracalibration/chess1.png img = cv.imread("chess1.png",cv.IMREAD_GRAYSCALE) +climg = cv.cvtColor(img, cv.COLOR_GRAY2BGR) # Find the chess board corners ret, corners = cv.findChessboardCorners(img, cv.Size{Int32}(7,5)) # If found, add object points, image points (after refining them) if ret - img = cv.drawChessboardCorners(img, cv.Size{Int32}(7,5), corners,ret) - cv.imshow("img",img) + climg = cv.drawChessboardCorners(climg, cv.Size{Int32}(7,5), corners,ret) + cv.imshow("img",climg) cv.waitKey(Int32(0)) cv.destroyAllWindows() From 7022f4e3e00c62a7768d18a8a725c1bd7cc32427 Mon Sep 17 00:00:00 2001 From: Akash Sharma Date: Tue, 13 Oct 2020 15:19:15 -0400 Subject: [PATCH 04/29] Merge pull request #2619 from akashsharma02:submap [GSoC] Add Submaps and PoseGraph optimization for Large Scale Depth Fusion * - Add HashTSDF class - Implement Integrate function (untested) * Integration seems to be working, raycasting does not * Update integration code * Integration and Raycasting fixes, (both work now) * - Format code - Clean up comments and few fixes * Add Kinect Fusion backup file * - Add interpolation for vertices and normals (slow and unreliable!) - Format code - Delete kinfu_back.cpp * Bug fix for integration and noisy odometry * - Create volume abstract class - Address Review comments * - Add getPoints and getNormals function - Fix formatting according to comments - Move volume abstract class to include/opencv2/rgbd/ - Write factory method for creating TSDFVolumes - Small bug fixes - Minor fixes according to comments * - Add tests for hashTSDF - Fix raycasting bug causing to loop forever - Suppress warnings by explicit conversion - Disable hashTsdf test until we figure out memory leak - style changes - Add missing license in a few files, correct precomp.hpp usage * - Use CRTP based static polymorphism to choose between CPU and GPU for HashTSDF volume * Create submap and submapMgr Implement overlap_ratio check to create new submaps * Early draft of posegraph and submaps (Doesn't even compile) * Minor cleanup (no compilation) * Track all submaps (no posegraph update yet) * Return inliers from ICP for weighting the constraints (Huber threshold based inliers pending) * Add updating constraints between submaps and retain same current map * Fix constraints creation between submaps and allow for switching between submaps * - Fix bug in allocate volumeUnits - Simplify calculation of visibleBlocks * Remove inlier calculation in fast_icp (not required) * Modify readFile to allow reading other datasets easily * - Implement posegraph update, Gauss newton is unstable - Minor changes to Gauss newton and Sparse matrix. Residual still increases slightly over iterations * Implement simplified levenberg marquardt * Bug fixes for Levenberg Marquardt and minor changes * minor changes * Fixes, but Optimizer is still not well behaved * Working Ceres optimizer * - Reorganize IO code for samples in a separate file - Minor fix for Ceres preprocessor definition - Remove unused generatorJacobian, will be used for opencv implementation of levenberg marquardt - Doxygen docs fix - Minor preprocessor fixes * - Reorganize IO code for samples in a separate file - Minor fix for Ceres preprocessor definition - Remove unused generatorJacobian, will be used for opencv implementation of levenberg marquardt - Doxygen docs fix - Minor preprocessor fixes - Move inline functions to header, and make function params const references * - Add Python bindings for volume struct - Remove makeVolume(const VolumeParams&) Python binding due to compilation issues - Minor changes according to comments * - Remove dynafu::Params() since it is identical to kinfu::Params() - Use common functions for dynafu_demo - Suppress "unreachable code" in volume.cpp * Minor API changes * Minor * Remove CRTP for HashTSDF class * Bug fixes for HashTSDF integration --- modules/rgbd/CMakeLists.txt | 9 + modules/rgbd/include/opencv2/rgbd.hpp | 1 + modules/rgbd/include/opencv2/rgbd/dynafu.hpp | 100 +--- modules/rgbd/include/opencv2/rgbd/kinfu.hpp | 16 +- .../rgbd/include/opencv2/rgbd/large_kinfu.hpp | 143 +++++ modules/rgbd/include/opencv2/rgbd/volume.hpp | 86 ++- modules/rgbd/samples/dynafu_demo.cpp | 204 +------ modules/rgbd/samples/io_utils.hpp | 313 ++++++++++ modules/rgbd/samples/kinfu_demo.cpp | 264 +-------- modules/rgbd/samples/large_kinfu_demo.cpp | 260 +++++++++ modules/rgbd/src/dynafu.cpp | 70 --- modules/rgbd/src/fast_icp.cpp | 3 - modules/rgbd/src/fast_icp.hpp | 1 - modules/rgbd/src/hash_tsdf.cpp | 152 +++-- modules/rgbd/src/hash_tsdf.hpp | 109 ++-- modules/rgbd/src/kinfu.cpp | 1 - modules/rgbd/src/large_kinfu.cpp | 361 ++++++++++++ modules/rgbd/src/pose_graph.cpp | 169 ++++++ modules/rgbd/src/pose_graph.hpp | 321 +++++++++++ modules/rgbd/src/sparse_block_matrix.hpp | 159 +++++ modules/rgbd/src/submap.hpp | 544 ++++++++++++++++++ modules/rgbd/src/tsdf.cpp | 83 +-- modules/rgbd/src/tsdf.hpp | 44 +- modules/rgbd/src/utils.hpp | 2 + modules/rgbd/src/volume.cpp | 77 ++- 25 files changed, 2651 insertions(+), 841 deletions(-) create mode 100644 modules/rgbd/include/opencv2/rgbd/large_kinfu.hpp create mode 100644 modules/rgbd/samples/io_utils.hpp create mode 100644 modules/rgbd/samples/large_kinfu_demo.cpp create mode 100644 modules/rgbd/src/large_kinfu.cpp create mode 100644 modules/rgbd/src/pose_graph.cpp create mode 100644 modules/rgbd/src/pose_graph.hpp create mode 100644 modules/rgbd/src/sparse_block_matrix.hpp create mode 100644 modules/rgbd/src/submap.hpp diff --git a/modules/rgbd/CMakeLists.txt b/modules/rgbd/CMakeLists.txt index 7f2f6a67257..247c788a9c1 100644 --- a/modules/rgbd/CMakeLists.txt +++ b/modules/rgbd/CMakeLists.txt @@ -1,2 +1,11 @@ set(the_description "RGBD algorithms") + +find_package(Ceres QUIET) ocv_define_module(rgbd opencv_core opencv_calib3d opencv_imgproc OPTIONAL opencv_viz WRAP python) +ocv_target_link_libraries(${the_module} ${CERES_LIBRARIES}) + +if(Ceres_FOUND) + ocv_target_compile_definitions(${the_module} PUBLIC CERES_FOUND) +else() + message(STATUS "CERES support is disabled. Ceres Solver is Required for Posegraph optimization") +endif() diff --git a/modules/rgbd/include/opencv2/rgbd.hpp b/modules/rgbd/include/opencv2/rgbd.hpp index 37b2927cdcf..d4ac749c2a5 100755 --- a/modules/rgbd/include/opencv2/rgbd.hpp +++ b/modules/rgbd/include/opencv2/rgbd.hpp @@ -13,6 +13,7 @@ #include "opencv2/rgbd/depth.hpp" #include "opencv2/rgbd/kinfu.hpp" #include "opencv2/rgbd/dynafu.hpp" +#include "opencv2/rgbd/large_kinfu.hpp" /** @defgroup rgbd RGB-Depth Processing diff --git a/modules/rgbd/include/opencv2/rgbd/dynafu.hpp b/modules/rgbd/include/opencv2/rgbd/dynafu.hpp index d057ebe76c6..fae69c48eef 100644 --- a/modules/rgbd/include/opencv2/rgbd/dynafu.hpp +++ b/modules/rgbd/include/opencv2/rgbd/dynafu.hpp @@ -10,103 +10,11 @@ #include "opencv2/core.hpp" #include "opencv2/core/affine.hpp" +#include "kinfu.hpp" + namespace cv { namespace dynafu { -struct CV_EXPORTS_W Params -{ - /** @brief Default parameters - A set of parameters which provides better model quality, can be very slow. - */ - CV_WRAP static Ptr defaultParams(); - - /** @brief Coarse parameters - A set of parameters which provides better speed, can fail to match frames - in case of rapid sensor motion. - */ - CV_WRAP static Ptr coarseParams(); - - /** @brief frame size in pixels */ - CV_PROP_RW Size frameSize; - - /** @brief camera intrinsics */ - CV_PROP Matx33f intr; - - /** @brief pre-scale per 1 meter for input values - - Typical values are: - * 5000 per 1 meter for the 16-bit PNG files of TUM database - * 1000 per 1 meter for Kinect 2 device - * 1 per 1 meter for the 32-bit float images in the ROS bag files - */ - CV_PROP_RW float depthFactor; - - /** @brief Depth sigma in meters for bilateral smooth */ - CV_PROP_RW float bilateral_sigma_depth; - /** @brief Spatial sigma in pixels for bilateral smooth */ - CV_PROP_RW float bilateral_sigma_spatial; - /** @brief Kernel size in pixels for bilateral smooth */ - CV_PROP_RW int bilateral_kernel_size; - - /** @brief Number of pyramid levels for ICP */ - CV_PROP_RW int pyramidLevels; - - /** @brief Resolution of voxel space - - Number of voxels in each dimension. - */ - CV_PROP_RW Vec3i volumeDims; - /** @brief Size of voxel in meters */ - CV_PROP_RW float voxelSize; - - /** @brief Minimal camera movement in meters - - Integrate new depth frame only if camera movement exceeds this value. - */ - CV_PROP_RW float tsdf_min_camera_movement; - - /** @brief initial volume pose in meters */ - Affine3f volumePose; - - /** @brief distance to truncate in meters - - Distances to surface that exceed this value will be truncated to 1.0. - */ - CV_PROP_RW float tsdf_trunc_dist; - - /** @brief max number of frames per voxel - - Each voxel keeps running average of distances no longer than this value. - */ - CV_PROP_RW int tsdf_max_weight; - - /** @brief A length of one raycast step - - How much voxel sizes we skip each raycast step - */ - CV_PROP_RW float raycast_step_factor; - - // gradient delta in voxel sizes - // fixed at 1.0f - // float gradient_delta_factor; - - /** @brief light pose for rendering in meters */ - CV_PROP Vec3f lightPose; - - /** @brief distance theshold for ICP in meters */ - CV_PROP_RW float icpDistThresh; - /** angle threshold for ICP in radians */ - CV_PROP_RW float icpAngleThresh; - /** number of ICP iterations for each pyramid level */ - CV_PROP std::vector icpIterations; - - /** @brief Threshold for depth truncation in meters - - All depth values beyond this threshold will be set to zero - */ - CV_PROP_RW float truncateThreshold; -}; - /** @brief DynamicFusion implementation This class implements a 3d reconstruction algorithm as described in @cite dynamicfusion. @@ -132,11 +40,11 @@ struct CV_EXPORTS_W Params class CV_EXPORTS_W DynaFu { public: - CV_WRAP static Ptr create(const Ptr& _params); + CV_WRAP static Ptr create(const Ptr& _params); virtual ~DynaFu(); /** @brief Get current parameters */ - virtual const Params& getParams() const = 0; + virtual const kinfu::Params& getParams() const = 0; /** @brief Renders a volume into an image diff --git a/modules/rgbd/include/opencv2/rgbd/kinfu.hpp b/modules/rgbd/include/opencv2/rgbd/kinfu.hpp index 8fcc3189ac3..95f732b6769 100644 --- a/modules/rgbd/include/opencv2/rgbd/kinfu.hpp +++ b/modules/rgbd/include/opencv2/rgbd/kinfu.hpp @@ -24,22 +24,22 @@ struct CV_EXPORTS_W Params /** * @brief Constructor for Params * Sets the initial pose of the TSDF volume. - * @param volumeIntialPoseRot rotation matrix - * @param volumeIntialPoseTransl translation vector + * @param volumeInitialPoseRot rotation matrix + * @param volumeInitialPoseTransl translation vector */ - CV_WRAP Params(Matx33f volumeIntialPoseRot, Vec3f volumeIntialPoseTransl) + CV_WRAP Params(Matx33f volumeInitialPoseRot, Vec3f volumeInitialPoseTransl) { - setInitialVolumePose(volumeIntialPoseRot,volumeIntialPoseTransl); + setInitialVolumePose(volumeInitialPoseRot,volumeInitialPoseTransl); } /** * @brief Constructor for Params * Sets the initial pose of the TSDF volume. - * @param volumeIntialPose 4 by 4 Homogeneous Transform matrix to set the intial pose of TSDF volume + * @param volumeInitialPose 4 by 4 Homogeneous Transform matrix to set the intial pose of TSDF volume */ - CV_WRAP Params(Matx44f volumeIntialPose) + CV_WRAP Params(Matx44f volumeInitialPose) { - setInitialVolumePose(volumeIntialPose); + setInitialVolumePose(volumeInitialPose); } /** @@ -77,7 +77,7 @@ struct CV_EXPORTS_W Params /** @brief frame size in pixels */ CV_PROP_RW Size frameSize; - CV_PROP_RW cv::kinfu::VolumeType volumeType; + CV_PROP_RW kinfu::VolumeType volumeType; /** @brief camera intrinsics */ CV_PROP_RW Matx33f intr; diff --git a/modules/rgbd/include/opencv2/rgbd/large_kinfu.hpp b/modules/rgbd/include/opencv2/rgbd/large_kinfu.hpp new file mode 100644 index 00000000000..7f1428c6d51 --- /dev/null +++ b/modules/rgbd/include/opencv2/rgbd/large_kinfu.hpp @@ -0,0 +1,143 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +// This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this +// module's directory + +#ifndef __OPENCV_RGBD_LARGEKINFU_HPP__ +#define __OPENCV_RGBD_LARGEKINFU_HPP__ + +#include + +#include "opencv2/core.hpp" +#include "opencv2/core/affine.hpp" + +namespace cv +{ +namespace large_kinfu +{ +struct CV_EXPORTS_W Params +{ + /** @brief Default parameters + A set of parameters which provides better model quality, can be very slow. + */ + CV_WRAP static Ptr defaultParams(); + + /** @brief Coarse parameters + A set of parameters which provides better speed, can fail to match frames + in case of rapid sensor motion. + */ + CV_WRAP static Ptr coarseParams(); + + /** @brief HashTSDF parameters + A set of parameters suitable for use with HashTSDFVolume + */ + CV_WRAP static Ptr hashTSDFParams(bool isCoarse); + + /** @brief frame size in pixels */ + CV_PROP_RW Size frameSize; + + /** @brief camera intrinsics */ + CV_PROP_RW Matx33f intr; + + /** @brief pre-scale per 1 meter for input values + Typical values are: + * 5000 per 1 meter for the 16-bit PNG files of TUM database + * 1000 per 1 meter for Kinect 2 device + * 1 per 1 meter for the 32-bit float images in the ROS bag files + */ + CV_PROP_RW float depthFactor; + + /** @brief Depth sigma in meters for bilateral smooth */ + CV_PROP_RW float bilateral_sigma_depth; + /** @brief Spatial sigma in pixels for bilateral smooth */ + CV_PROP_RW float bilateral_sigma_spatial; + /** @brief Kernel size in pixels for bilateral smooth */ + CV_PROP_RW int bilateral_kernel_size; + + /** @brief Number of pyramid levels for ICP */ + CV_PROP_RW int pyramidLevels; + + /** @brief Minimal camera movement in meters + Integrate new depth frame only if camera movement exceeds this value. + */ + CV_PROP_RW float tsdf_min_camera_movement; + + /** @brief light pose for rendering in meters */ + CV_PROP_RW Vec3f lightPose; + + /** @brief distance theshold for ICP in meters */ + CV_PROP_RW float icpDistThresh; + /** @brief angle threshold for ICP in radians */ + CV_PROP_RW float icpAngleThresh; + /** @brief number of ICP iterations for each pyramid level */ + CV_PROP_RW std::vector icpIterations; + + /** @brief Threshold for depth truncation in meters + All depth values beyond this threshold will be set to zero + */ + CV_PROP_RW float truncateThreshold; + + /** @brief Volume parameters + */ + kinfu::VolumeParams volumeParams; +}; + +/** @brief Large Scale Dense Depth Fusion implementation + + This class implements a 3d reconstruction algorithm for larger environments using + Spatially hashed TSDF volume "Submaps". + It also runs a periodic posegraph optimization to minimize drift in tracking over long sequences. + Currently the algorithm does not implement a relocalization or loop closure module. + Potentially a Bag of words implementation or RGBD relocalization as described in + Glocker et al. ISMAR 2013 will be implemented + + It takes a sequence of depth images taken from depth sensor + (or any depth images source such as stereo camera matching algorithm or even raymarching + renderer). The output can be obtained as a vector of points and their normals or can be + Phong-rendered from given camera pose. + + An internal representation of a model is a spatially hashed voxel cube that stores TSDF values + which represent the distance to the closest surface (for details read the @cite kinectfusion article + about TSDF). There is no interface to that representation yet. + + For posegraph optimization, a Submap abstraction over the Volume class is created. + New submaps are added to the model when there is low visibility overlap between current viewing frustrum + and the existing volume/model. Multiple submaps are simultaneously tracked and a posegraph is created and + optimized periodically. + + LargeKinfu does not use any OpenCL acceleration yet. + To enable or disable it explicitly use cv::setUseOptimized() or cv::ocl::setUseOpenCL(). + + This implementation is inspired from Kintinuous, InfiniTAM and other SOTA algorithms + + You need to set the OPENCV_ENABLE_NONFREE option in CMake to use KinectFusion. +*/ +class CV_EXPORTS_W LargeKinfu +{ + public: + CV_WRAP static Ptr create(const Ptr& _params); + virtual ~LargeKinfu() = default; + + virtual const Params& getParams() const = 0; + + CV_WRAP virtual void render(OutputArray image, + const Matx44f& cameraPose = Matx44f::eye()) const = 0; + + CV_WRAP virtual void getCloud(OutputArray points, OutputArray normals) const = 0; + + CV_WRAP virtual void getPoints(OutputArray points) const = 0; + + CV_WRAP virtual void getNormals(InputArray points, OutputArray normals) const = 0; + + CV_WRAP virtual void reset() = 0; + + virtual const Affine3f getPose() const = 0; + + CV_WRAP virtual bool update(InputArray depth) = 0; +}; + +} // namespace large_kinfu +} // namespace cv +#endif diff --git a/modules/rgbd/include/opencv2/rgbd/volume.hpp b/modules/rgbd/include/opencv2/rgbd/volume.hpp index 3d10e2dd99c..33f63b1fbfb 100644 --- a/modules/rgbd/include/opencv2/rgbd/volume.hpp +++ b/modules/rgbd/include/opencv2/rgbd/volume.hpp @@ -18,7 +18,7 @@ namespace kinfu class CV_EXPORTS_W Volume { public: - Volume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor) + Volume(float _voxelSize, Matx44f _pose, float _raycastStepFactor) : voxelSize(_voxelSize), voxelSizeInv(1.0f / voxelSize), pose(_pose), @@ -28,19 +28,18 @@ class CV_EXPORTS_W Volume virtual ~Volume(){}; - virtual void integrate(InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, - const cv::kinfu::Intr& intrinsics) = 0; - virtual void raycast(const cv::Matx44f& cameraPose, const cv::kinfu::Intr& intrinsics, - cv::Size frameSize, cv::OutputArray points, - cv::OutputArray normals) const = 0; - virtual void fetchNormals(cv::InputArray points, cv::OutputArray _normals) const = 0; - virtual void fetchPointsNormals(cv::OutputArray points, cv::OutputArray normals) const = 0; - virtual void reset() = 0; + virtual void integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, + const kinfu::Intr& intrinsics, const int frameId = 0) = 0; + virtual void raycast(const Matx44f& cameraPose, const kinfu::Intr& intrinsics, + const Size& frameSize, OutputArray points, OutputArray normals) const = 0; + virtual void fetchNormals(InputArray points, OutputArray _normals) const = 0; + virtual void fetchPointsNormals(OutputArray points, OutputArray normals) const = 0; + virtual void reset() = 0; public: const float voxelSize; const float voxelSizeInv; - const cv::Affine3f pose; + const Affine3f pose; const float raycastStepFactor; }; @@ -50,9 +49,70 @@ enum class VolumeType HASHTSDF = 1 }; -CV_EXPORTS_W cv::Ptr makeVolume(VolumeType _volumeType, float _voxelSize, cv::Matx44f _pose, - float _raycastStepFactor, float _truncDist, int _maxWeight, - float _truncateThreshold, Vec3i _resolution); +struct CV_EXPORTS_W VolumeParams +{ + /** @brief Type of Volume + Values can be TSDF (single volume) or HASHTSDF (hashtable of volume units) + */ + CV_PROP_RW VolumeType type; + + /** @brief Resolution of voxel space + Number of voxels in each dimension. + Applicable only for TSDF Volume. + HashTSDF volume only supports equal resolution in all three dimensions + */ + CV_PROP_RW Vec3i resolution; + + /** @brief Resolution of volumeUnit in voxel space + Number of voxels in each dimension for volumeUnit + Applicable only for hashTSDF. + */ + CV_PROP_RW int unitResolution = {0}; + + /** @brief Initial pose of the volume in meters */ + Affine3f pose; + + /** @brief Length of voxels in meters */ + CV_PROP_RW float voxelSize; + + /** @brief TSDF truncation distance + Distances greater than value from surface will be truncated to 1.0 + */ + CV_PROP_RW float tsdfTruncDist; + + /** @brief Max number of frames to integrate per voxel + Represents the max number of frames over which a running average + of the TSDF is calculated for a voxel + */ + CV_PROP_RW int maxWeight; + + /** @brief Threshold for depth truncation in meters + Truncates the depth greater than threshold to 0 + */ + CV_PROP_RW float depthTruncThreshold; + + /** @brief Length of single raycast step + Describes the percentage of voxel length that is skipped per march + */ + CV_PROP_RW float raycastStepFactor; + + /** @brief Default set of parameters that provide higher quality reconstruction + at the cost of slow performance. + */ + CV_WRAP static Ptr defaultParams(VolumeType _volumeType); + + /** @brief Coarse set of parameters that provides relatively higher performance + at the cost of reconstrution quality. + */ + CV_WRAP static Ptr coarseParams(VolumeType _volumeType); +}; + + +Ptr makeVolume(const VolumeParams& _volumeParams); +CV_EXPORTS_W Ptr makeVolume(VolumeType _volumeType, float _voxelSize, Matx44f _pose, + float _raycastStepFactor, float _truncDist, int _maxWeight, + float _truncateThreshold, Vec3i _resolution); + } // namespace kinfu } // namespace cv #endif diff --git a/modules/rgbd/samples/dynafu_demo.cpp b/modules/rgbd/samples/dynafu_demo.cpp index b69d8725f9c..8b27021ea5a 100644 --- a/modules/rgbd/samples/dynafu_demo.cpp +++ b/modules/rgbd/samples/dynafu_demo.cpp @@ -13,208 +13,16 @@ #include #include #include +#include "io_utils.hpp" using namespace cv; using namespace cv::dynafu; -using namespace std; +using namespace cv::io_utils; #ifdef HAVE_OPENCV_VIZ #include #endif -static vector readDepth(std::string fileList); - -static vector readDepth(std::string fileList) -{ - vector v; - - fstream file(fileList); - if(!file.is_open()) - throw std::runtime_error("Failed to read depth list"); - - std::string dir; - size_t slashIdx = fileList.rfind('/'); - slashIdx = slashIdx != std::string::npos ? slashIdx : fileList.rfind('\\'); - dir = fileList.substr(0, slashIdx); - - while(!file.eof()) - { - std::string s, imgPath; - std::getline(file, s); - if(s.empty() || s[0] == '#') continue; - std::stringstream ss; - ss << s; - double thumb; - ss >> thumb >> imgPath; - v.push_back(dir+'/'+imgPath); - } - - return v; -} - -struct DepthWriter -{ - DepthWriter(string fileList) : - file(fileList, ios::out), count(0), dir() - { - size_t slashIdx = fileList.rfind('/'); - slashIdx = slashIdx != std::string::npos ? slashIdx : fileList.rfind('\\'); - dir = fileList.substr(0, slashIdx); - - if(!file.is_open()) - throw std::runtime_error("Failed to write depth list"); - - file << "# depth maps saved from device" << endl; - file << "# useless_number filename" << endl; - } - - void append(InputArray _depth) - { - Mat depth = _depth.getMat(); - string depthFname = cv::format("%04d.png", count); - string fullDepthFname = dir + '/' + depthFname; - if(!imwrite(fullDepthFname, depth)) - throw std::runtime_error("Failed to write depth to file " + fullDepthFname); - file << count++ << " " << depthFname << endl; - } - - fstream file; - int count; - string dir; -}; - -namespace Kinect2Params -{ - static const Size frameSize = Size(512, 424); - // approximate values, no guarantee to be correct - static const float focal = 366.1f; - static const float cx = 258.2f; - static const float cy = 204.f; - static const float k1 = 0.12f; - static const float k2 = -0.34f; - static const float k3 = 0.12f; -}; - -struct DepthSource -{ -public: - DepthSource(int cam) : - DepthSource("", cam) - { } - - DepthSource(String fileListName) : - DepthSource(fileListName, -1) - { } - - DepthSource(String fileListName, int cam) : - depthFileList(fileListName.empty() ? vector() : readDepth(fileListName)), - frameIdx(0), - vc( cam >= 0 ? VideoCapture(VideoCaptureAPIs::CAP_OPENNI2 + cam) : VideoCapture()), - undistortMap1(), - undistortMap2(), - useKinect2Workarounds(true) - { - } - - UMat getDepth() - { - UMat out; - if (!vc.isOpened()) - { - if (frameIdx < depthFileList.size()) - { - Mat f = cv::imread(depthFileList[frameIdx++], IMREAD_ANYDEPTH); - f.copyTo(out); - } - else - { - return UMat(); - } - } - else - { - vc.grab(); - vc.retrieve(out, CAP_OPENNI_DEPTH_MAP); - - // workaround for Kinect 2 - if(useKinect2Workarounds) - { - out = out(Rect(Point(), Kinect2Params::frameSize)); - - UMat outCopy; - // linear remap adds gradient between valid and invalid pixels - // which causes garbage, use nearest instead - remap(out, outCopy, undistortMap1, undistortMap2, cv::INTER_NEAREST); - - cv::flip(outCopy, out, 1); - } - } - if (out.empty()) - throw std::runtime_error("Matrix is empty"); - return out; - } - - bool empty() - { - return depthFileList.empty() && !(vc.isOpened()); - } - - void updateParams(Params& params) - { - if (vc.isOpened()) - { - // this should be set in according to user's depth sensor - int w = (int)vc.get(VideoCaptureProperties::CAP_PROP_FRAME_WIDTH); - int h = (int)vc.get(VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT); - - float focal = (float)vc.get(CAP_OPENNI_DEPTH_GENERATOR | CAP_PROP_OPENNI_FOCAL_LENGTH); - - // it's recommended to calibrate sensor to obtain its intrinsics - float fx, fy, cx, cy; - Size frameSize; - if(useKinect2Workarounds) - { - fx = fy = Kinect2Params::focal; - cx = Kinect2Params::cx; - cy = Kinect2Params::cy; - - frameSize = Kinect2Params::frameSize; - } - else - { - fx = fy = focal; - cx = w/2 - 0.5f; - cy = h/2 - 0.5f; - - frameSize = Size(w, h); - } - - Matx33f camMatrix = Matx33f(fx, 0, cx, - 0, fy, cy, - 0, 0, 1); - - params.frameSize = frameSize; - params.intr = camMatrix; - params.depthFactor = 1000.f; - - Matx distCoeffs; - distCoeffs(0) = Kinect2Params::k1; - distCoeffs(1) = Kinect2Params::k2; - distCoeffs(4) = Kinect2Params::k3; - if(useKinect2Workarounds) - initUndistortRectifyMap(camMatrix, distCoeffs, cv::noArray(), - camMatrix, frameSize, CV_16SC2, - undistortMap1, undistortMap2); - } - } - - vector depthFileList; - size_t frameIdx; - VideoCapture vc; - UMat undistortMap1, undistortMap2; - bool useKinect2Workarounds; -}; - #ifdef HAVE_OPENCV_VIZ const std::string vizWindowName = "cloud"; @@ -265,7 +73,7 @@ int main(int argc, char **argv) { bool coarse = false; bool idle = false; - string recordPath; + std::string recordPath; CommandLineParser parser(argc, argv, keys); parser.about(message); @@ -312,13 +120,13 @@ int main(int argc, char **argv) if(!recordPath.empty()) depthWriter = makePtr(recordPath); - Ptr params; + Ptr params; Ptr df; if(coarse) - params = Params::coarseParams(); + params = kinfu::Params::coarseParams(); else - params = Params::defaultParams(); + params = kinfu::Params::defaultParams(); // These params can be different for each depth sensor ds->updateParams(*params); diff --git a/modules/rgbd/samples/io_utils.hpp b/modules/rgbd/samples/io_utils.hpp new file mode 100644 index 00000000000..c96d6c5345d --- /dev/null +++ b/modules/rgbd/samples/io_utils.hpp @@ -0,0 +1,313 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +#ifndef OPENCV_RGBS_IO_UTILS_HPP +#define OPENCV_RGBS_IO_UTILS_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace cv +{ +namespace io_utils +{ + +static std::vector readDepth(const std::string& fileList) +{ + std::vector v; + + std::fstream file(fileList); + if (!file.is_open()) + throw std::runtime_error("Failed to read depth list"); + + std::string dir; + size_t slashIdx = fileList.rfind('/'); + slashIdx = slashIdx != std::string::npos ? slashIdx : fileList.rfind('\\'); + dir = fileList.substr(0, slashIdx); + + while (!file.eof()) + { + std::string s, imgPath; + std::getline(file, s); + if (s.empty() || s[0] == '#') + continue; + std::stringstream ss; + ss << s; + double thumb; + ss >> thumb >> imgPath; + v.push_back(dir + '/' + imgPath); + } + + return v; +} + +struct DepthWriter +{ + DepthWriter(std::string fileList) : file(fileList, std::ios::out), count(0), dir() + { + size_t slashIdx = fileList.rfind('/'); + slashIdx = slashIdx != std::string::npos ? slashIdx : fileList.rfind('\\'); + dir = fileList.substr(0, slashIdx); + + if (!file.is_open()) + throw std::runtime_error("Failed to write depth list"); + + file << "# depth maps saved from device" << std::endl; + file << "# useless_number filename" << std::endl; + } + + void append(InputArray _depth) + { + Mat depth = _depth.getMat(); + std::string depthFname = cv::format("%04d.png", count); + std::string fullDepthFname = dir + '/' + depthFname; + if (!imwrite(fullDepthFname, depth)) + throw std::runtime_error("Failed to write depth to file " + fullDepthFname); + file << count++ << " " << depthFname << std::endl; + } + + std::fstream file; + int count; + std::string dir; +}; + +namespace Kinect2Params +{ +static const Size frameSize = Size(512, 424); +// approximate values, no guarantee to be correct +static const float focal = 366.1f; +static const float cx = 258.2f; +static const float cy = 204.f; +static const float k1 = 0.12f; +static const float k2 = -0.34f; +static const float k3 = 0.12f; +}; // namespace Kinect2Params + +struct DepthSource +{ + public: + enum Type + { + DEPTH_LIST, + DEPTH_KINECT2_LIST, + DEPTH_KINECT2, + DEPTH_REALSENSE + }; + + DepthSource(int cam) : DepthSource("", cam) {} + + DepthSource(String fileListName) : DepthSource(fileListName, -1) {} + + DepthSource(String fileListName, int cam) + : depthFileList(fileListName.empty() ? std::vector() + : readDepth(fileListName)), + frameIdx(0), + undistortMap1(), + undistortMap2() + { + if (cam >= 0) + { + vc = VideoCapture(VideoCaptureAPIs::CAP_OPENNI2 + cam); + if (vc.isOpened()) + { + sourceType = Type::DEPTH_KINECT2; + } + else + { + vc = VideoCapture(VideoCaptureAPIs::CAP_REALSENSE + cam); + if (vc.isOpened()) + { + sourceType = Type::DEPTH_REALSENSE; + } + } + } + else + { + vc = VideoCapture(); + sourceType = Type::DEPTH_KINECT2_LIST; + } + } + + UMat getDepth() + { + UMat out; + if (!vc.isOpened()) + { + if (frameIdx < depthFileList.size()) + { + Mat f = cv::imread(depthFileList[frameIdx++], IMREAD_ANYDEPTH); + f.copyTo(out); + } + else + { + return UMat(); + } + } + else + { + vc.grab(); + switch (sourceType) + { + case Type::DEPTH_KINECT2: vc.retrieve(out, CAP_OPENNI_DEPTH_MAP); break; + case Type::DEPTH_REALSENSE: vc.retrieve(out, CAP_INTELPERC_DEPTH_MAP); break; + default: + // unknown depth source + vc.retrieve(out); + } + + // workaround for Kinect 2 + if (sourceType == Type::DEPTH_KINECT2) + { + out = out(Rect(Point(), Kinect2Params::frameSize)); + + UMat outCopy; + // linear remap adds gradient between valid and invalid pixels + // which causes garbage, use nearest instead + remap(out, outCopy, undistortMap1, undistortMap2, cv::INTER_NEAREST); + + cv::flip(outCopy, out, 1); + } + } + if (out.empty()) + throw std::runtime_error("Matrix is empty"); + return out; + } + + bool empty() { return depthFileList.empty() && !(vc.isOpened()); } + + void updateIntrinsics(Matx33f& _intrinsics, Size& _frameSize, float& _depthFactor) + { + if (vc.isOpened()) + { + // this should be set in according to user's depth sensor + int w = (int)vc.get(VideoCaptureProperties::CAP_PROP_FRAME_WIDTH); + int h = (int)vc.get(VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT); + + // it's recommended to calibrate sensor to obtain its intrinsics + float fx, fy, cx, cy; + float depthFactor = 1000.f; + Size frameSize; + if (sourceType == Type::DEPTH_KINECT2) + { + fx = fy = Kinect2Params::focal; + cx = Kinect2Params::cx; + cy = Kinect2Params::cy; + + frameSize = Kinect2Params::frameSize; + } + else + { + if (sourceType == Type::DEPTH_REALSENSE) + { + fx = (float)vc.get(CAP_PROP_INTELPERC_DEPTH_FOCAL_LENGTH_HORZ); + fy = (float)vc.get(CAP_PROP_INTELPERC_DEPTH_FOCAL_LENGTH_VERT); + depthFactor = 1.f / (float)vc.get(CAP_PROP_INTELPERC_DEPTH_SATURATION_VALUE); + } + else + { + fx = fy = + (float)vc.get(CAP_OPENNI_DEPTH_GENERATOR | CAP_PROP_OPENNI_FOCAL_LENGTH); + } + + cx = w / 2 - 0.5f; + cy = h / 2 - 0.5f; + + frameSize = Size(w, h); + } + + Matx33f camMatrix = Matx33f(fx, 0, cx, 0, fy, cy, 0, 0, 1); + _intrinsics = camMatrix; + _frameSize = frameSize; + _depthFactor = depthFactor; + } + } + + void updateVolumeParams(const Vec3i& _resolution, float& _voxelSize, float& _tsdfTruncDist, + Affine3f& _volumePose, float& _depthTruncateThreshold) + { + float volumeSize = 3.0f; + _depthTruncateThreshold = 0.0f; + // RealSense has shorter depth range, some params should be tuned + if (sourceType == Type::DEPTH_REALSENSE) + { + volumeSize = 1.f; + _voxelSize = volumeSize / _resolution[0]; + _tsdfTruncDist = 0.01f; + _depthTruncateThreshold = 2.5f; + } + _volumePose = Affine3f().translate(Vec3f(-volumeSize / 2.f, -volumeSize / 2.f, 0.05f)); + } + + void updateICPParams(float& _icpDistThresh, float& _bilateralSigmaDepth) + { + _icpDistThresh = 0.1f; + _bilateralSigmaDepth = 0.04f; + // RealSense has shorter depth range, some params should be tuned + if (sourceType == Type::DEPTH_REALSENSE) + { + _icpDistThresh = 0.01f; + _bilateralSigmaDepth = 0.01f; + } + } + + void updateParams(large_kinfu::Params& params) + { + if (vc.isOpened()) + { + updateIntrinsics(params.intr, params.frameSize, params.depthFactor); + updateVolumeParams(params.volumeParams.resolution, params.volumeParams.voxelSize, + params.volumeParams.tsdfTruncDist, params.volumeParams.pose, + params.truncateThreshold); + updateICPParams(params.icpDistThresh, params.bilateral_sigma_depth); + + if (sourceType == Type::DEPTH_KINECT2) + { + Matx distCoeffs; + distCoeffs(0) = Kinect2Params::k1; + distCoeffs(1) = Kinect2Params::k2; + distCoeffs(4) = Kinect2Params::k3; + + initUndistortRectifyMap(params.intr, distCoeffs, cv::noArray(), params.intr, + params.frameSize, CV_16SC2, undistortMap1, undistortMap2); + } + } + } + + void updateParams(kinfu::Params& params) + { + if (vc.isOpened()) + { + updateIntrinsics(params.intr, params.frameSize, params.depthFactor); + updateVolumeParams(params.volumeDims, params.voxelSize, + params.tsdf_trunc_dist, params.volumePose, params.truncateThreshold); + updateICPParams(params.icpDistThresh, params.bilateral_sigma_depth); + + if (sourceType == Type::DEPTH_KINECT2) + { + Matx distCoeffs; + distCoeffs(0) = Kinect2Params::k1; + distCoeffs(1) = Kinect2Params::k2; + distCoeffs(4) = Kinect2Params::k3; + + initUndistortRectifyMap(params.intr, distCoeffs, cv::noArray(), params.intr, + params.frameSize, CV_16SC2, undistortMap1, undistortMap2); + } + } + } + + std::vector depthFileList; + size_t frameIdx; + VideoCapture vc; + UMat undistortMap1, undistortMap2; + Type sourceType; +}; +} // namespace io_utils + +} // namespace cv +#endif /* ifndef OPENCV_RGBS_IO_UTILS_HPP */ diff --git a/modules/rgbd/samples/kinfu_demo.cpp b/modules/rgbd/samples/kinfu_demo.cpp index 147ffaa2d8e..e264ba38bbe 100644 --- a/modules/rgbd/samples/kinfu_demo.cpp +++ b/modules/rgbd/samples/kinfu_demo.cpp @@ -11,272 +11,16 @@ #include #include +#include "io_utils.hpp" + using namespace cv; using namespace cv::kinfu; -using namespace std; +using namespace cv::io_utils; #ifdef HAVE_OPENCV_VIZ #include #endif -static vector readDepth(std::string fileList); - -static vector readDepth(std::string fileList) -{ - vector v; - - fstream file(fileList); - if(!file.is_open()) - throw std::runtime_error("Failed to read depth list"); - - std::string dir; - size_t slashIdx = fileList.rfind('/'); - slashIdx = slashIdx != std::string::npos ? slashIdx : fileList.rfind('\\'); - dir = fileList.substr(0, slashIdx); - - while(!file.eof()) - { - std::string s, imgPath; - std::getline(file, s); - if(s.empty() || s[0] == '#') continue; - std::stringstream ss; - ss << s; - double thumb; - ss >> thumb >> imgPath; - v.push_back(dir+'/'+imgPath); - } - - return v; -} - -struct DepthWriter -{ - DepthWriter(string fileList) : - file(fileList, ios::out), count(0), dir() - { - size_t slashIdx = fileList.rfind('/'); - slashIdx = slashIdx != std::string::npos ? slashIdx : fileList.rfind('\\'); - dir = fileList.substr(0, slashIdx); - - if(!file.is_open()) - throw std::runtime_error("Failed to write depth list"); - - file << "# depth maps saved from device" << endl; - file << "# useless_number filename" << endl; - } - - void append(InputArray _depth) - { - Mat depth = _depth.getMat(); - string depthFname = cv::format("%04d.png", count); - string fullDepthFname = dir + '/' + depthFname; - if(!imwrite(fullDepthFname, depth)) - throw std::runtime_error("Failed to write depth to file " + fullDepthFname); - file << count++ << " " << depthFname << endl; - } - - fstream file; - int count; - string dir; -}; - -namespace Kinect2Params -{ - static const Size frameSize = Size(512, 424); - // approximate values, no guarantee to be correct - static const float focal = 366.1f; - static const float cx = 258.2f; - static const float cy = 204.f; - static const float k1 = 0.12f; - static const float k2 = -0.34f; - static const float k3 = 0.12f; -}; - -struct DepthSource -{ -public: - enum Type - { - DEPTH_LIST, - DEPTH_KINECT2_LIST, - DEPTH_KINECT2, - DEPTH_REALSENSE - }; - - DepthSource(int cam) : - DepthSource("", cam) - { } - - DepthSource(String fileListName) : - DepthSource(fileListName, -1) - { } - - DepthSource(String fileListName, int cam) : - depthFileList(fileListName.empty() ? vector() : readDepth(fileListName)), - frameIdx(0), - undistortMap1(), - undistortMap2() - { - if(cam >= 0) - { - vc = VideoCapture(VideoCaptureAPIs::CAP_OPENNI2 + cam); - if(vc.isOpened()) - { - sourceType = Type::DEPTH_KINECT2; - } - else - { - vc = VideoCapture(VideoCaptureAPIs::CAP_REALSENSE + cam); - if(vc.isOpened()) - { - sourceType = Type::DEPTH_REALSENSE; - } - } - } - else - { - vc = VideoCapture(); - sourceType = Type::DEPTH_KINECT2_LIST; - } - } - - UMat getDepth() - { - UMat out; - if (!vc.isOpened()) - { - if (frameIdx < depthFileList.size()) - { - Mat f = cv::imread(depthFileList[frameIdx++], IMREAD_ANYDEPTH); - f.copyTo(out); - } - else - { - return UMat(); - } - } - else - { - vc.grab(); - switch (sourceType) - { - case Type::DEPTH_KINECT2: - vc.retrieve(out, CAP_OPENNI_DEPTH_MAP); - break; - case Type::DEPTH_REALSENSE: - vc.retrieve(out, CAP_INTELPERC_DEPTH_MAP); - break; - default: - // unknown depth source - vc.retrieve(out); - } - - // workaround for Kinect 2 - if(sourceType == Type::DEPTH_KINECT2) - { - out = out(Rect(Point(), Kinect2Params::frameSize)); - - UMat outCopy; - // linear remap adds gradient between valid and invalid pixels - // which causes garbage, use nearest instead - remap(out, outCopy, undistortMap1, undistortMap2, cv::INTER_NEAREST); - - cv::flip(outCopy, out, 1); - } - } - if (out.empty()) - throw std::runtime_error("Matrix is empty"); - return out; - } - - bool empty() - { - return depthFileList.empty() && !(vc.isOpened()); - } - - void updateParams(Params& params) - { - if (vc.isOpened()) - { - // this should be set in according to user's depth sensor - int w = (int)vc.get(VideoCaptureProperties::CAP_PROP_FRAME_WIDTH); - int h = (int)vc.get(VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT); - - // it's recommended to calibrate sensor to obtain its intrinsics - float fx, fy, cx, cy; - float depthFactor = 1000.f; - Size frameSize; - if(sourceType == Type::DEPTH_KINECT2) - { - fx = fy = Kinect2Params::focal; - cx = Kinect2Params::cx; - cy = Kinect2Params::cy; - - frameSize = Kinect2Params::frameSize; - } - else - { - if(sourceType == Type::DEPTH_REALSENSE) - { - fx = (float)vc.get(CAP_PROP_INTELPERC_DEPTH_FOCAL_LENGTH_HORZ); - fy = (float)vc.get(CAP_PROP_INTELPERC_DEPTH_FOCAL_LENGTH_VERT); - depthFactor = 1.f/(float)vc.get(CAP_PROP_INTELPERC_DEPTH_SATURATION_VALUE); - } - else - { - fx = fy = (float)vc.get(CAP_OPENNI_DEPTH_GENERATOR | CAP_PROP_OPENNI_FOCAL_LENGTH); - } - - cx = w/2 - 0.5f; - cy = h/2 - 0.5f; - - frameSize = Size(w, h); - } - - Matx33f camMatrix = Matx33f(fx, 0, cx, - 0, fy, cy, - 0, 0, 1); - - params.frameSize = frameSize; - params.intr = camMatrix; - params.depthFactor = depthFactor; - - // RealSense has shorter depth range, some params should be tuned - if(sourceType == Type::DEPTH_REALSENSE) - { - // all sizes in meters - float cubeSize = 1.f; - params.voxelSize = cubeSize/params.volumeDims[0]; - params.tsdf_trunc_dist = 0.01f; - params.icpDistThresh = 0.01f; - params.volumePose = Affine3f().translate(Vec3f(-cubeSize/2.f, - -cubeSize/2.f, - 0.05f)); - params.truncateThreshold = 2.5f; - params.bilateral_sigma_depth = 0.01f; - } - - if(sourceType == Type::DEPTH_KINECT2) - { - Matx distCoeffs; - distCoeffs(0) = Kinect2Params::k1; - distCoeffs(1) = Kinect2Params::k2; - distCoeffs(4) = Kinect2Params::k3; - - initUndistortRectifyMap(camMatrix, distCoeffs, cv::noArray(), - camMatrix, frameSize, CV_16SC2, - undistortMap1, undistortMap2); - } - } - } - - vector depthFileList; - size_t frameIdx; - VideoCapture vc; - UMat undistortMap1, undistortMap2; - Type sourceType; -}; - #ifdef HAVE_OPENCV_VIZ const std::string vizWindowName = "cloud"; @@ -329,7 +73,7 @@ int main(int argc, char **argv) bool coarse = false; bool idle = false; bool useHashTSDF = false; - string recordPath; + std::string recordPath; CommandLineParser parser(argc, argv, keys); parser.about(message); diff --git a/modules/rgbd/samples/large_kinfu_demo.cpp b/modules/rgbd/samples/large_kinfu_demo.cpp new file mode 100644 index 00000000000..40d14ca8cae --- /dev/null +++ b/modules/rgbd/samples/large_kinfu_demo.cpp @@ -0,0 +1,260 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +// This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this +// module's directory + +#include +#include +#include +#include +#include +#include + +#include "io_utils.hpp" + +using namespace cv; +using namespace cv::kinfu; +using namespace cv::large_kinfu; +using namespace cv::io_utils; + +#ifdef HAVE_OPENCV_VIZ +#include +#endif + +#ifdef HAVE_OPENCV_VIZ +const std::string vizWindowName = "cloud"; + +struct PauseCallbackArgs +{ + PauseCallbackArgs(LargeKinfu& _largeKinfu) : largeKinfu(_largeKinfu) {} + + LargeKinfu& largeKinfu; +}; + +void pauseCallback(const viz::MouseEvent& me, void* args); +void pauseCallback(const viz::MouseEvent& me, void* args) +{ + if (me.type == viz::MouseEvent::Type::MouseMove || + me.type == viz::MouseEvent::Type::MouseScrollDown || + me.type == viz::MouseEvent::Type::MouseScrollUp) + { + PauseCallbackArgs pca = *((PauseCallbackArgs*)(args)); + viz::Viz3d window(vizWindowName); + UMat rendered; + pca.largeKinfu.render(rendered, window.getViewerPose().matrix); + imshow("render", rendered); + waitKey(1); + } +} +#endif + +static const char* keys = { + "{help h usage ? | | print this message }" + "{depth | | Path to depth.txt file listing a set of depth images }" + "{camera |0| Index of depth camera to be used as a depth source }" + "{coarse | | Run on coarse settings (fast but ugly) or on default (slow but looks better)," + " in coarse mode points and normals are displayed }" + "{idle | | Do not run LargeKinfu, just display depth frames }" + "{record | | Write depth frames to specified file list" + " (the same format as for the 'depth' key) }" +}; + +static const std::string message = + "\nThis demo uses live depth input or RGB-D dataset taken from" + "\nhttps://vision.in.tum.de/data/datasets/rgbd-dataset" + "\nto demonstrate Submap based large environment reconstruction" + "\nThis module uses the newer hashtable based TSDFVolume (relatively fast) for larger " + "reconstructions by default\n"; + +int main(int argc, char** argv) +{ + bool coarse = false; + bool idle = false; + std::string recordPath; + + CommandLineParser parser(argc, argv, keys); + parser.about(message); + + if (!parser.check()) + { + parser.printMessage(); + parser.printErrors(); + return -1; + } + + if (parser.has("help")) + { + parser.printMessage(); + return 0; + } + if (parser.has("coarse")) + { + coarse = true; + } + if (parser.has("record")) + { + recordPath = parser.get("record"); + } + if (parser.has("idle")) + { + idle = true; + } + + Ptr ds; + if (parser.has("depth")) + ds = makePtr(parser.get("depth")); + else + ds = makePtr(parser.get("camera")); + + if (ds->empty()) + { + std::cerr << "Failed to open depth source" << std::endl; + parser.printMessage(); + return -1; + } + + Ptr depthWriter; + if (!recordPath.empty()) + depthWriter = makePtr(recordPath); + + Ptr params; + Ptr largeKinfu; + + params = large_kinfu::Params::hashTSDFParams(coarse); + + // These params can be different for each depth sensor + ds->updateParams(*params); + + // Disabled until there is no OpenCL accelerated HashTSDF is available + cv::setUseOptimized(false); + + if (!idle) + largeKinfu = LargeKinfu::create(params); + +#ifdef HAVE_OPENCV_VIZ + cv::viz::Viz3d window(vizWindowName); + window.setViewerPose(Affine3f::Identity()); + bool pause = false; +#endif + + UMat rendered; + UMat points; + UMat normals; + + int64 prevTime = getTickCount(); + + for (UMat frame = ds->getDepth(); !frame.empty(); frame = ds->getDepth()) + { + if (depthWriter) + depthWriter->append(frame); + +#ifdef HAVE_OPENCV_VIZ + if (pause) + { + // doesn't happen in idle mode + largeKinfu->getCloud(points, normals); + if (!points.empty() && !normals.empty()) + { + viz::WCloud cloudWidget(points, viz::Color::white()); + viz::WCloudNormals cloudNormals(points, normals, /*level*/ 1, /*scale*/ 0.05, + viz::Color::gray()); + window.showWidget("cloud", cloudWidget); + window.showWidget("normals", cloudNormals); + + Vec3d volSize = largeKinfu->getParams().volumeParams.voxelSize * + Vec3d(largeKinfu->getParams().volumeParams.resolution); + window.showWidget("cube", viz::WCube(Vec3d::all(0), volSize), + largeKinfu->getParams().volumeParams.pose); + PauseCallbackArgs pca(*largeKinfu); + window.registerMouseCallback(pauseCallback, (void*)&pca); + window.showWidget("text", + viz::WText(cv::String("Move camera in this window. " + "Close the window or press Q to resume"), + Point())); + window.spin(); + window.removeWidget("text"); + window.removeWidget("cloud"); + window.removeWidget("normals"); + window.registerMouseCallback(0); + } + + pause = false; + } + else +#endif + { + UMat cvt8; + float depthFactor = params->depthFactor; + convertScaleAbs(frame, cvt8, 0.25 * 256. / depthFactor); + if (!idle) + { + imshow("depth", cvt8); + + if (!largeKinfu->update(frame)) + { + largeKinfu->reset(); + std::cout << "reset" << std::endl; + } +#ifdef HAVE_OPENCV_VIZ + else + { + if (coarse) + { + largeKinfu->getCloud(points, normals); + if (!points.empty() && !normals.empty()) + { + viz::WCloud cloudWidget(points, viz::Color::white()); + viz::WCloudNormals cloudNormals(points, normals, /*level*/ 1, + /*scale*/ 0.05, viz::Color::gray()); + window.showWidget("cloud", cloudWidget); + window.showWidget("normals", cloudNormals); + } + } + + // window.showWidget("worldAxes", viz::WCoordinateSystem()); + Vec3d volSize = largeKinfu->getParams().volumeParams.voxelSize * + largeKinfu->getParams().volumeParams.resolution; + window.showWidget("cube", viz::WCube(Vec3d::all(0), volSize), + largeKinfu->getParams().volumeParams.pose); + window.setViewerPose(largeKinfu->getPose()); + window.spinOnce(1, true); + } +#endif + + largeKinfu->render(rendered); + } + else + { + rendered = cvt8; + } + } + + int64 newTime = getTickCount(); + putText(rendered, + cv::format("FPS: %2d press R to reset, P to pause, Q to quit", + (int)(getTickFrequency() / (newTime - prevTime))), + Point(0, rendered.rows - 1), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 255)); + prevTime = newTime; + imshow("render", rendered); + + int c = waitKey(1); + switch (c) + { + case 'r': + if (!idle) + largeKinfu->reset(); + break; + case 'q': return 0; +#ifdef HAVE_OPENCV_VIZ + case 'p': + if (!idle) + pause = true; +#endif + default: break; + } + } + + return 0; +} diff --git a/modules/rgbd/src/dynafu.cpp b/modules/rgbd/src/dynafu.cpp index d663275d32d..002be7bd6d7 100644 --- a/modules/rgbd/src/dynafu.cpp +++ b/modules/rgbd/src/dynafu.cpp @@ -82,76 +82,6 @@ namespace cv { namespace dynafu { using namespace kinfu; -Ptr Params::defaultParams() -{ - Params p; - - p.frameSize = Size(640, 480); - - float fx, fy, cx, cy; - fx = fy = 525.f; - cx = p.frameSize.width/2 - 0.5f; - cy = p.frameSize.height/2 - 0.5f; - p.intr = Matx33f(fx, 0, cx, - 0, fy, cy, - 0, 0, 1); - - // 5000 for the 16-bit PNG files - // 1 for the 32-bit float images in the ROS bag files - p.depthFactor = 5000; - - // sigma_depth is scaled by depthFactor when calling bilateral filter - p.bilateral_sigma_depth = 0.04f; //meter - p.bilateral_sigma_spatial = 4.5; //pixels - p.bilateral_kernel_size = 7; //pixels - - p.icpAngleThresh = (float)(30. * CV_PI / 180.); // radians - p.icpDistThresh = 0.1f; // meters - - p.icpIterations = {10, 5, 4}; - p.pyramidLevels = (int)p.icpIterations.size(); - - p.tsdf_min_camera_movement = 0.f; //meters, disabled - - p.volumeDims = Vec3i::all(512); //number of voxels - - float volSize = 3.f; - p.voxelSize = volSize/512.f; //meters - - // default pose of volume cube - p.volumePose = Affine3f().translate(Vec3f(-volSize/2.f, -volSize/2.f, 0.5f)); - p.tsdf_trunc_dist = 0.04f; //meters; - p.tsdf_max_weight = 64; //frames - - p.raycast_step_factor = 0.25f; //in voxel sizes - // gradient delta factor is fixed at 1.0f and is not used - //p.gradient_delta_factor = 0.5f; //in voxel sizes - - //p.lightPose = p.volume_pose.translation()/4; //meters - p.lightPose = Vec3f::all(0.f); //meters - - // depth truncation is not used by default but can be useful in some scenes - p.truncateThreshold = 0.f; //meters - - return makePtr(p); -} - -Ptr Params::coarseParams() -{ - Ptr p = defaultParams(); - - p->icpIterations = {5, 3, 2}; - p->pyramidLevels = (int)p->icpIterations.size(); - - float volSize = 3.f; - p->volumeDims = Vec3i::all(128); //number of voxels - p->voxelSize = volSize/128.f; - - p->raycast_step_factor = 0.75f; //in voxel sizes - - return p; -} - // T should be Mat or UMat template< typename T > class DynaFuImpl : public DynaFu diff --git a/modules/rgbd/src/fast_icp.cpp b/modules/rgbd/src/fast_icp.cpp index 06d4a750647..ab25eb94f0d 100644 --- a/modules/rgbd/src/fast_icp.cpp +++ b/modules/rgbd/src/fast_icp.cpp @@ -32,7 +32,6 @@ class ICPImpl : public ICP InputArray oldPoints, InputArray oldNormals, InputArray newPoints, InputArray newNormals ) const override; - template < typename T > bool estimateTransformT(cv::Affine3f& transform, const vector& oldPoints, const vector& oldNormals, @@ -298,7 +297,6 @@ struct GetAbInvoker : ParallelLoopBody continue; // build point-wise vector ab = [ A | b ] - v_float32x4 VxNv = crossProduct(newP, oldN); Point3f VxN; VxN.x = VxNv.get0(); @@ -449,7 +447,6 @@ struct GetAbInvoker : ParallelLoopBody //try to optimize Point3f VxN = newP.cross(oldN); float ab[7] = {VxN.x, VxN.y, VxN.z, oldN.x, oldN.y, oldN.z, oldN.dot(-diff)}; - // build point-wise upper-triangle matrix [ab^T * ab] w/o last row // which is [A^T*A | A^T*b] // and gather sum diff --git a/modules/rgbd/src/fast_icp.hpp b/modules/rgbd/src/fast_icp.hpp index 7a9f0694096..a11a919da3e 100644 --- a/modules/rgbd/src/fast_icp.hpp +++ b/modules/rgbd/src/fast_icp.hpp @@ -22,7 +22,6 @@ class ICP InputArray oldPoints, InputArray oldNormals, InputArray newPoints, InputArray newNormals ) const = 0; - virtual ~ICP() { } protected: diff --git a/modules/rgbd/src/hash_tsdf.cpp b/modules/rgbd/src/hash_tsdf.cpp index b414500217f..69baad8e360 100644 --- a/modules/rgbd/src/hash_tsdf.cpp +++ b/modules/rgbd/src/hash_tsdf.cpp @@ -37,9 +37,8 @@ static inline float tsdfToFloat(TsdfType num) return float(num) * (-1.f / 128.f); } -HashTSDFVolume::HashTSDFVolume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, - float _truncDist, int _maxWeight, float _truncateThreshold, - int _volumeUnitRes, bool _zFirstMemOrder) +HashTSDFVolume::HashTSDFVolume(float _voxelSize, const Matx44f& _pose, float _raycastStepFactor, float _truncDist, + int _maxWeight, float _truncateThreshold, int _volumeUnitRes, bool _zFirstMemOrder) : Volume(_voxelSize, _pose, _raycastStepFactor), maxWeight(_maxWeight), truncateThreshold(_truncateThreshold), @@ -50,14 +49,18 @@ HashTSDFVolume::HashTSDFVolume(float _voxelSize, cv::Matx44f _pose, float _rayca truncDist = std::max(_truncDist, 4.0f * voxelSize); } -HashTSDFVolumeCPU::HashTSDFVolumeCPU(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, - float _truncDist, int _maxWeight, float _truncateThreshold, - int _volumeUnitRes, bool _zFirstMemOrder) - : HashTSDFVolume(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, - _truncateThreshold, _volumeUnitRes, _zFirstMemOrder) +HashTSDFVolumeCPU::HashTSDFVolumeCPU(float _voxelSize, const Matx44f& _pose, float _raycastStepFactor, float _truncDist, + int _maxWeight, float _truncateThreshold, int _volumeUnitRes, bool _zFirstMemOrder) + :HashTSDFVolume(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, _truncateThreshold, _volumeUnitRes, + _zFirstMemOrder) { } +HashTSDFVolumeCPU::HashTSDFVolumeCPU(const VolumeParams& _params, bool _zFirstMemOrder) + : HashTSDFVolume(_params.voxelSize, _params.pose.matrix, _params.raycastStepFactor, _params.tsdfTruncDist, _params.maxWeight, + _params.depthTruncThreshold, _params.unitResolution, _zFirstMemOrder) +{ +} // zero volume, leave rest params the same void HashTSDFVolumeCPU::reset() { @@ -65,7 +68,7 @@ void HashTSDFVolumeCPU::reset() volumeUnits.clear(); } -void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, const Intr& intrinsics) +void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, const Intr& intrinsics, const int frameId) { CV_TRACE_FUNCTION(); @@ -73,7 +76,7 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma Depth depth = _depth.getMat(); //! Compute volumes to be allocated - const int depthStride = 1; + const int depthStride = int(log2(volumeUnitResolution)); const float invDepthFactor = 1.f / depthFactor; const Intr::Reprojector reproj(intrinsics.makeReprojector()); const Affine3f cam2vol(pose.inv() * Affine3f(cameraPose)); @@ -135,6 +138,7 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma Matx44f subvolumePose = pose.translate(volumeUnitIdxToVolume(idx)).matrix; vu.pVolume = makePtr(voxelSize, subvolumePose, raycastStepFactor, truncDist, maxWeight, volumeDims); //! This volume unit will definitely be required for current integration + vu.lastVisibleIndex = frameId; vu.isActive = true; } @@ -169,7 +173,8 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma if (cameraPoint.x >= 0 && cameraPoint.y >= 0 && cameraPoint.x < depth.cols && cameraPoint.y < depth.rows) { assert(it != volumeUnits.end()); - it->second.isActive = true; + it->second.lastVisibleIndex = frameId; + it->second.isActive = true; } } }); @@ -195,34 +200,34 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma }); } -cv::Vec3i HashTSDFVolumeCPU::volumeToVolumeUnitIdx(cv::Point3f p) const +cv::Vec3i HashTSDFVolumeCPU::volumeToVolumeUnitIdx(const cv::Point3f& p) const { return cv::Vec3i(cvFloor(p.x / volumeUnitSize), cvFloor(p.y / volumeUnitSize), cvFloor(p.z / volumeUnitSize)); } -cv::Point3f HashTSDFVolumeCPU::volumeUnitIdxToVolume(cv::Vec3i volumeUnitIdx) const +cv::Point3f HashTSDFVolumeCPU::volumeUnitIdxToVolume(const cv::Vec3i& volumeUnitIdx) const { return cv::Point3f(volumeUnitIdx[0] * volumeUnitSize, volumeUnitIdx[1] * volumeUnitSize, volumeUnitIdx[2] * volumeUnitSize); } -cv::Point3f HashTSDFVolumeCPU::voxelCoordToVolume(cv::Vec3i voxelIdx) const +cv::Point3f HashTSDFVolumeCPU::voxelCoordToVolume(const cv::Vec3i& voxelIdx) const { return cv::Point3f(voxelIdx[0] * voxelSize, voxelIdx[1] * voxelSize, voxelIdx[2] * voxelSize); } -cv::Vec3i HashTSDFVolumeCPU::volumeToVoxelCoord(cv::Point3f point) const +cv::Vec3i HashTSDFVolumeCPU::volumeToVoxelCoord(const cv::Point3f& point) const { return cv::Vec3i(cvFloor(point.x * voxelSizeInv), cvFloor(point.y * voxelSizeInv), cvFloor(point.z * voxelSizeInv)); } -inline TsdfVoxel HashTSDFVolumeCPU::at(const cv::Vec3i& volumeIdx) const +TsdfVoxel HashTSDFVolumeCPU::at(const Vec3i& volumeIdx) const { - cv::Vec3i volumeUnitIdx = cv::Vec3i(cvFloor(volumeIdx[0] / volumeUnitResolution), - cvFloor(volumeIdx[1] / volumeUnitResolution), - cvFloor(volumeIdx[2] / volumeUnitResolution)); + Vec3i volumeUnitIdx = Vec3i(cvFloor(volumeIdx[0] / volumeUnitResolution), + cvFloor(volumeIdx[1] / volumeUnitResolution), + cvFloor(volumeIdx[2] / volumeUnitResolution)); VolumeUnitMap::const_iterator it = volumeUnits.find(volumeUnitIdx); if (it == volumeUnits.end()) @@ -232,21 +237,19 @@ inline TsdfVoxel HashTSDFVolumeCPU::at(const cv::Vec3i& volumeIdx) const dummy.weight = 0; return dummy; } - cv::Ptr volumeUnit = - std::dynamic_pointer_cast(it->second.pVolume); + Ptr volumeUnit = std::dynamic_pointer_cast(it->second.pVolume); - cv::Vec3i volUnitLocalIdx = volumeIdx - cv::Vec3i(volumeUnitIdx[0] * volumeUnitResolution, - volumeUnitIdx[1] * volumeUnitResolution, - volumeUnitIdx[2] * volumeUnitResolution); + Vec3i volUnitLocalIdx = volumeIdx - Vec3i(volumeUnitIdx[0] * volumeUnitResolution, + volumeUnitIdx[1] * volumeUnitResolution, + volumeUnitIdx[2] * volumeUnitResolution); - volUnitLocalIdx = - cv::Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); + volUnitLocalIdx = Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); return volumeUnit->at(volUnitLocalIdx); } -inline TsdfVoxel HashTSDFVolumeCPU::at(const cv::Point3f& point) const +TsdfVoxel HashTSDFVolumeCPU::at(const Point3f& point) const { - cv::Vec3i volumeUnitIdx = volumeToVolumeUnitIdx(point); + Vec3i volumeUnitIdx = volumeToVolumeUnitIdx(point); VolumeUnitMap::const_iterator it = volumeUnits.find(volumeUnitIdx); if (it == volumeUnits.end()) { @@ -255,13 +258,11 @@ inline TsdfVoxel HashTSDFVolumeCPU::at(const cv::Point3f& point) const dummy.weight = 0; return dummy; } - cv::Ptr volumeUnit = - std::dynamic_pointer_cast(it->second.pVolume); + Ptr volumeUnit = std::dynamic_pointer_cast(it->second.pVolume); - cv::Point3f volumeUnitPos = volumeUnitIdxToVolume(volumeUnitIdx); - cv::Vec3i volUnitLocalIdx = volumeToVoxelCoord(point - volumeUnitPos); - volUnitLocalIdx = - cv::Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); + Point3f volumeUnitPos = volumeUnitIdxToVolume(volumeUnitIdx); + Vec3i volUnitLocalIdx = volumeToVoxelCoord(point - volumeUnitPos); + volUnitLocalIdx = Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); return volumeUnit->at(volUnitLocalIdx); } @@ -386,7 +387,8 @@ inline float HashTSDFVolumeCPU::interpolateVoxel(const cv::Point3f& point) const return interpolateVoxelPoint(point * voxelSizeInv); } -inline Point3f HashTSDFVolumeCPU::getNormalVoxel(Point3f point) const + +Point3f HashTSDFVolumeCPU::getNormalVoxel(const Point3f &point) const { Vec3f normal = Vec3f(0, 0, 0); @@ -527,8 +529,8 @@ inline Point3f HashTSDFVolumeCPU::getNormalVoxel(Point3f point) const struct HashRaycastInvoker : ParallelLoopBody { - HashRaycastInvoker(Points& _points, Normals& _normals, const Matx44f& cameraPose, - const Intr& intrinsics, const HashTSDFVolumeCPU& _volume) + HashRaycastInvoker(Points& _points, Normals& _normals, const Matx44f& cameraPose, const Intr& intrinsics, + const HashTSDFVolumeCPU& _volume) : ParallelLoopBody(), points(_points), normals(_normals), @@ -559,41 +561,38 @@ struct HashRaycastInvoker : ParallelLoopBody Point3f point = nan3, normal = nan3; //! Ray origin and direction in the volume coordinate frame - Point3f orig = cam2volTrans; - Point3f rayDirV = - normalize(Vec3f(cam2volRot * reproj(Point3f(float(x), float(y), 1.f)))); + Point3f orig = cam2volTrans; + Point3f rayDirV = normalize(Vec3f(cam2volRot * reproj(Point3f(float(x), float(y), 1.f)))); float tmin = 0; float tmax = volume.truncateThreshold; float tcurr = tmin; - cv::Vec3i prevVolumeUnitIdx = - cv::Vec3i(std::numeric_limits::min(), std::numeric_limits::min(), - std::numeric_limits::min()); + Vec3i prevVolumeUnitIdx = + Vec3i(std::numeric_limits::min(), std::numeric_limits::min(), + std::numeric_limits::min()); float tprev = tcurr; float prevTsdf = volume.truncDist; - cv::Ptr currVolumeUnit; + Ptr currVolumeUnit; while (tcurr < tmax) { - Point3f currRayPos = orig + tcurr * rayDirV; - cv::Vec3i currVolumeUnitIdx = volume.volumeToVolumeUnitIdx(currRayPos); + Point3f currRayPos = orig + tcurr * rayDirV; + Vec3i currVolumeUnitIdx = volume.volumeToVolumeUnitIdx(currRayPos); VolumeUnitMap::const_iterator it = volume.volumeUnits.find(currVolumeUnitIdx); float currTsdf = prevTsdf; int currWeight = 0; float stepSize = 0.5f * blockSize; - cv::Vec3i volUnitLocalIdx; + Vec3i volUnitLocalIdx; - //! Does the subvolume exist in hashtable + //! The subvolume exists in hashtable if (it != volume.volumeUnits.end()) { - currVolumeUnit = - std::dynamic_pointer_cast(it->second.pVolume); - cv::Point3f currVolUnitPos = - volume.volumeUnitIdxToVolume(currVolumeUnitIdx); - volUnitLocalIdx = volume.volumeToVoxelCoord(currRayPos - currVolUnitPos); + currVolumeUnit = std::dynamic_pointer_cast(it->second.pVolume); + Point3f currVolUnitPos = volume.volumeUnitIdxToVolume(currVolumeUnitIdx); + volUnitLocalIdx = volume.volumeToVoxelCoord(currRayPos - currVolUnitPos); //! TODO: Figure out voxel interpolation TsdfVoxel currVoxel = currVolumeUnit->at(volUnitLocalIdx); @@ -604,8 +603,7 @@ struct HashRaycastInvoker : ParallelLoopBody //! Surface crossing if (prevTsdf > 0.f && currTsdf <= 0.f && currWeight > 0) { - float tInterp = - (tcurr * prevTsdf - tprev * currTsdf) / (prevTsdf - currTsdf); + float tInterp = (tcurr * prevTsdf - tprev * currTsdf) / (prevTsdf - currTsdf); if (!cvIsNaN(tInterp) && !cvIsInf(tInterp)) { Point3f pv = orig + tInterp * rayDirV; @@ -639,9 +637,8 @@ struct HashRaycastInvoker : ParallelLoopBody const Intr::Reprojector reproj; }; -void HashTSDFVolumeCPU::raycast(const cv::Matx44f& cameraPose, const cv::kinfu::Intr& intrinsics, - cv::Size frameSize, cv::OutputArray _points, - cv::OutputArray _normals) const +void HashTSDFVolumeCPU::raycast(const Matx44f& cameraPose, const kinfu::Intr& intrinsics, const Size& frameSize, + OutputArray _points, OutputArray _normals) const { CV_TRACE_FUNCTION(); CV_Assert(frameSize.area() > 0); @@ -660,10 +657,9 @@ void HashTSDFVolumeCPU::raycast(const cv::Matx44f& cameraPose, const cv::kinfu:: struct HashFetchPointsNormalsInvoker : ParallelLoopBody { - HashFetchPointsNormalsInvoker(const HashTSDFVolumeCPU& _volume, - const std::vector& _totalVolUnits, - std::vector>& _pVecs, - std::vector>& _nVecs, bool _needNormals) + HashFetchPointsNormalsInvoker(const HashTSDFVolumeCPU& _volume, const std::vector& _totalVolUnits, + std::vector>& _pVecs, std::vector>& _nVecs, + bool _needNormals) : ParallelLoopBody(), volume(_volume), totalVolUnits(_totalVolUnits), @@ -678,21 +674,20 @@ struct HashFetchPointsNormalsInvoker : ParallelLoopBody std::vector points, normals; for (int i = range.start; i < range.end; i++) { - cv::Vec3i tsdf_idx = totalVolUnits[i]; + Vec3i tsdf_idx = totalVolUnits[i]; VolumeUnitMap::const_iterator it = volume.volumeUnits.find(tsdf_idx); Point3f base_point = volume.volumeUnitIdxToVolume(tsdf_idx); if (it != volume.volumeUnits.end()) { - cv::Ptr volumeUnit = - std::dynamic_pointer_cast(it->second.pVolume); + Ptr volumeUnit = std::dynamic_pointer_cast(it->second.pVolume); std::vector localPoints; std::vector localNormals; for (int x = 0; x < volume.volumeUnitResolution; x++) for (int y = 0; y < volume.volumeUnitResolution; y++) for (int z = 0; z < volume.volumeUnitResolution; z++) { - cv::Vec3i voxelIdx(x, y, z); + Vec3i voxelIdx(x, y, z); TsdfVoxel voxel = volumeUnit->at(voxelIdx); if (voxel.tsdf != -128 && voxel.weight != 0) @@ -715,7 +710,7 @@ struct HashFetchPointsNormalsInvoker : ParallelLoopBody } const HashTSDFVolumeCPU& volume; - std::vector totalVolUnits; + std::vector totalVolUnits; std::vector>& pVecs; std::vector>& nVecs; const TsdfVoxel* volDataStart; @@ -760,7 +755,7 @@ void HashTSDFVolumeCPU::fetchPointsNormals(OutputArray _points, OutputArray _nor } } -void HashTSDFVolumeCPU::fetchNormals(cv::InputArray _points, cv::OutputArray _normals) const +void HashTSDFVolumeCPU::fetchNormals(InputArray _points, OutputArray _normals) const { CV_TRACE_FUNCTION(); @@ -773,8 +768,7 @@ void HashTSDFVolumeCPU::fetchNormals(cv::InputArray _points, cv::OutputArray _no Normals normals = _normals.getMat(); const HashTSDFVolumeCPU& _volume = *this; - auto HashPushNormals = [&](const ptype& point, const int* position) - { + auto HashPushNormals = [&](const ptype& point, const int* position) { const HashTSDFVolumeCPU& volume(_volume); Affine3f invPose(volume.pose.inv()); Point3f p = fromPtype(point); @@ -782,7 +776,7 @@ void HashTSDFVolumeCPU::fetchNormals(cv::InputArray _points, cv::OutputArray _no if (!isNaN(p)) { Point3f voxelPoint = invPose * p; - n = volume.pose.rotation() * volume.getNormalVoxel(voxelPoint); + n = volume.pose.rotation() * volume.getNormalVoxel(voxelPoint); } normals(position[0], position[1]) = toPtype(n); }; @@ -790,13 +784,17 @@ void HashTSDFVolumeCPU::fetchNormals(cv::InputArray _points, cv::OutputArray _no } } -cv::Ptr makeHashTSDFVolume(float _voxelSize, cv::Matx44f _pose, - float _raycastStepFactor, float _truncDist, - int _maxWeight, float _truncateThreshold, - int _volumeUnitResolution) +int HashTSDFVolumeCPU::getVisibleBlocks(int currFrameId, int frameThreshold) const { - return cv::makePtr(_voxelSize, _pose, _raycastStepFactor, _truncDist, - _maxWeight, _truncateThreshold, _volumeUnitResolution); + int numVisibleBlocks = 0; + //! TODO: Iterate over map parallely? + for (const auto& keyvalue : volumeUnits) + { + const VolumeUnit& volumeUnit = keyvalue.second; + if (volumeUnit.lastVisibleIndex > (currFrameId - frameThreshold)) + numVisibleBlocks++; + } + return numVisibleBlocks; } } // namespace kinfu diff --git a/modules/rgbd/src/hash_tsdf.hpp b/modules/rgbd/src/hash_tsdf.hpp index bac5ea9a46a..cbe4cffea03 100644 --- a/modules/rgbd/src/hash_tsdf.hpp +++ b/modules/rgbd/src/hash_tsdf.hpp @@ -15,40 +15,20 @@ namespace cv { namespace kinfu { -class HashTSDFVolume : public Volume -{ - public: - // dimension in voxels, size in meters - //! Use fixed volume cuboid - HashTSDFVolume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, float _truncDist, - int _maxWeight, float _truncateThreshold, int _volumeUnitRes, - bool zFirstMemOrder = true); - - virtual ~HashTSDFVolume() = default; - - public: - int maxWeight; - float truncDist; - float truncateThreshold; - int volumeUnitResolution; - float volumeUnitSize; - bool zFirstMemOrder; -}; - struct VolumeUnit { VolumeUnit() : pVolume(nullptr){}; ~VolumeUnit() = default; - cv::Ptr pVolume; - cv::Vec3i index; + Ptr pVolume; + int lastVisibleIndex = 0; bool isActive; }; //! Spatial hashing struct tsdf_hash { - size_t operator()(const cv::Vec3i& x) const noexcept + size_t operator()(const Vec3i& x) const noexcept { size_t seed = 0; constexpr uint32_t GOLDEN_RATIO = 0x9e3779b9; @@ -60,54 +40,87 @@ struct tsdf_hash } }; -typedef std::unordered_set VolumeUnitIndexSet; -typedef std::unordered_map VolumeUnitMap; +typedef std::unordered_set VolumeUnitIndexSet; +typedef std::unordered_map VolumeUnitMap; + +class HashTSDFVolume : public Volume +{ + public: + // dimension in voxels, size in meters + //! Use fixed volume cuboid + HashTSDFVolume(float _voxelSize, const Matx44f& _pose, float _raycastStepFactor, float _truncDist, + int _maxWeight, float _truncateThreshold, int _volumeUnitRes, + bool zFirstMemOrder = true); + + virtual ~HashTSDFVolume() = default; + + public: + int maxWeight; + float truncDist; + float truncateThreshold; + int volumeUnitResolution; + float volumeUnitSize; + bool zFirstMemOrder; +}; class HashTSDFVolumeCPU : public HashTSDFVolume { + public: // dimension in voxels, size in meters - HashTSDFVolumeCPU(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, - float _truncDist, int _maxWeight, float _truncateThreshold, - int _volumeUnitRes, bool zFirstMemOrder = true); + HashTSDFVolumeCPU(float _voxelSize, const Matx44f& _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, + float _truncateThreshold, int _volumeUnitRes, bool zFirstMemOrder = true); - virtual void integrate(InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, - const cv::kinfu::Intr& intrinsics) override; - virtual void raycast(const cv::Matx44f& cameraPose, const cv::kinfu::Intr& intrinsics, - cv::Size frameSize, cv::OutputArray points, - cv::OutputArray normals) const override; + HashTSDFVolumeCPU(const VolumeParams& _volumeParams, bool zFirstMemOrder = true); - virtual void fetchNormals(cv::InputArray points, cv::OutputArray _normals) const override; - virtual void fetchPointsNormals(cv::OutputArray points, cv::OutputArray normals) const override; + void integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, const kinfu::Intr& intrinsics, + const int frameId = 0) override; + void raycast(const Matx44f& cameraPose, const kinfu::Intr& intrinsics, const Size& frameSize, OutputArray points, + OutputArray normals) const override; - virtual void reset() override; + void fetchNormals(InputArray points, OutputArray _normals) const override; + void fetchPointsNormals(OutputArray points, OutputArray normals) const override; + + void reset() override; + size_t getTotalVolumeUnits() const { return volumeUnits.size(); } + int getVisibleBlocks(int currFrameId, int frameThreshold) const; //! Return the voxel given the voxel index in the universal volume (1 unit = 1 voxel_length) - virtual TsdfVoxel at(const cv::Vec3i& volumeIdx) const; + TsdfVoxel at(const Vec3i& volumeIdx) const; //! Return the voxel given the point in volume coordinate system i.e., (metric scale 1 unit = //! 1m) - virtual TsdfVoxel at(const cv::Point3f& point) const; + TsdfVoxel at(const Point3f& point) const; float interpolateVoxelPoint(const Point3f& point) const; - inline float interpolateVoxel(const cv::Point3f& point) const; - Point3f getNormalVoxel(cv::Point3f p) const; + float interpolateVoxel(const cv::Point3f& point) const; + Point3f getNormalVoxel(const cv::Point3f& p) const; //! Utility functions for coordinate transformations - cv::Vec3i volumeToVolumeUnitIdx(cv::Point3f point) const; - cv::Point3f volumeUnitIdxToVolume(cv::Vec3i volumeUnitIdx) const; + Vec3i volumeToVolumeUnitIdx(const Point3f& point) const; + Point3f volumeUnitIdxToVolume(const Vec3i& volumeUnitIdx) const; - cv::Point3f voxelCoordToVolume(cv::Vec3i voxelIdx) const; - cv::Vec3i volumeToVoxelCoord(cv::Point3f point) const; + Point3f voxelCoordToVolume(const Vec3i& voxelIdx) const; + Vec3i volumeToVoxelCoord(const Point3f& point) const; public: //! Hashtable of individual smaller volume units VolumeUnitMap volumeUnits; }; -cv::Ptr makeHashTSDFVolume(float _voxelSize, cv::Matx44f _pose, - float _raycastStepFactor, float _truncDist, - int _maxWeight, float truncateThreshold, - int volumeUnitResolution = 16); + +template +Ptr makeHashTSDFVolume(const VolumeParams& _volumeParams) +{ + return makePtr(_volumeParams); +} + +template +Ptr makeHashTSDFVolume(float _voxelSize, Matx44f _pose, float _raycastStepFactor, float _truncDist, + int _maxWeight, float truncateThreshold, int volumeUnitResolution = 16) +{ + return makePtr(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, truncateThreshold, + volumeUnitResolution); +} } // namespace kinfu } // namespace cv #endif diff --git a/modules/rgbd/src/kinfu.cpp b/modules/rgbd/src/kinfu.cpp index d776f359ec3..1d8314aae6f 100644 --- a/modules/rgbd/src/kinfu.cpp +++ b/modules/rgbd/src/kinfu.cpp @@ -119,7 +119,6 @@ class KinFuImpl : public KinFu void render(OutputArray image, const Matx44f& cameraPose) const CV_OVERRIDE; - //! TODO(Akash): Add back later virtual void getCloud(OutputArray points, OutputArray normals) const CV_OVERRIDE; void getPoints(OutputArray points) const CV_OVERRIDE; void getNormals(InputArray points, OutputArray normals) const CV_OVERRIDE; diff --git a/modules/rgbd/src/large_kinfu.cpp b/modules/rgbd/src/large_kinfu.cpp new file mode 100644 index 00000000000..dedeabb82db --- /dev/null +++ b/modules/rgbd/src/large_kinfu.cpp @@ -0,0 +1,361 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +// This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this +// module's directory + +#include "fast_icp.hpp" +#include "hash_tsdf.hpp" +#include "kinfu_frame.hpp" +#include "precomp.hpp" +#include "submap.hpp" +#include "tsdf.hpp" + +namespace cv +{ +namespace large_kinfu +{ +using namespace kinfu; + +Ptr Params::defaultParams() +{ + Params p; + + //! Frame parameters + { + p.frameSize = Size(640, 480); + + float fx, fy, cx, cy; + fx = fy = 525.f; + cx = p.frameSize.width / 2.0f - 0.5f; + cy = p.frameSize.height / 2.0f - 0.5f; + p.intr = Matx33f(fx, 0, cx, 0, fy, cy, 0, 0, 1); + + // 5000 for the 16-bit PNG files + // 1 for the 32-bit float images in the ROS bag files + p.depthFactor = 5000; + + // sigma_depth is scaled by depthFactor when calling bilateral filter + p.bilateral_sigma_depth = 0.04f; // meter + p.bilateral_sigma_spatial = 4.5; // pixels + p.bilateral_kernel_size = 7; // pixels + p.truncateThreshold = 0.f; // meters + } + //! ICP parameters + { + p.icpAngleThresh = (float)(30. * CV_PI / 180.); // radians + p.icpDistThresh = 0.1f; // meters + + p.icpIterations = { 10, 5, 4 }; + p.pyramidLevels = (int)p.icpIterations.size(); + } + //! Volume parameters + { + float volumeSize = 3.0f; + p.volumeParams.type = VolumeType::TSDF; + p.volumeParams.resolution = Vec3i::all(512); + p.volumeParams.pose = Affine3f().translate(Vec3f(-volumeSize / 2.f, -volumeSize / 2.f, 0.5f)); + p.volumeParams.voxelSize = volumeSize / 512.f; // meters + p.volumeParams.tsdfTruncDist = 7 * p.volumeParams.voxelSize; // about 0.04f in meters + p.volumeParams.maxWeight = 64; // frames + p.volumeParams.raycastStepFactor = 0.25f; // in voxel sizes + p.volumeParams.depthTruncThreshold = p.truncateThreshold; + } + //! Unused parameters + p.tsdf_min_camera_movement = 0.f; // meters, disabled + p.lightPose = Vec3f::all(0.f); // meters + + return makePtr(p); +} + +Ptr Params::coarseParams() +{ + Ptr p = defaultParams(); + + //! Reduce ICP iterations and pyramid levels + { + p->icpIterations = { 5, 3, 2 }; + p->pyramidLevels = (int)p->icpIterations.size(); + } + //! Make the volume coarse + { + float volumeSize = 3.f; + p->volumeParams.resolution = Vec3i::all(128); // number of voxels + p->volumeParams.voxelSize = volumeSize / 128.f; + p->volumeParams.tsdfTruncDist = 2 * p->volumeParams.voxelSize; // 0.04f in meters + p->volumeParams.raycastStepFactor = 0.75f; // in voxel sizes + } + return p; +} +Ptr Params::hashTSDFParams(bool isCoarse) +{ + Ptr p; + if (isCoarse) + p = coarseParams(); + else + p = defaultParams(); + + p->volumeParams.type = VolumeType::HASHTSDF; + p->volumeParams.depthTruncThreshold = rgbd::Odometry::DEFAULT_MAX_DEPTH(); + p->volumeParams.unitResolution = 16; + return p; +} + +// MatType should be Mat or UMat +template +class LargeKinfuImpl : public LargeKinfu +{ + public: + LargeKinfuImpl(const Params& _params); + virtual ~LargeKinfuImpl(); + + const Params& getParams() const CV_OVERRIDE; + + void render(OutputArray image, const Matx44f& cameraPose) const CV_OVERRIDE; + + virtual void getCloud(OutputArray points, OutputArray normals) const CV_OVERRIDE; + void getPoints(OutputArray points) const CV_OVERRIDE; + void getNormals(InputArray points, OutputArray normals) const CV_OVERRIDE; + + void reset() CV_OVERRIDE; + + const Affine3f getPose() const CV_OVERRIDE; + + bool update(InputArray depth) CV_OVERRIDE; + + bool updateT(const MatType& depth); + + private: + Params params; + + cv::Ptr icp; + //! TODO: Submap manager and Pose graph optimizer + cv::Ptr> submapMgr; + + int frameCounter; + Affine3f pose; +}; + +template +LargeKinfuImpl::LargeKinfuImpl(const Params& _params) + : params(_params) +{ + icp = makeICP(params.intr, params.icpIterations, params.icpAngleThresh, params.icpDistThresh); + + submapMgr = cv::makePtr>(params.volumeParams); + reset(); + submapMgr->createNewSubmap(true); + +} + +template +void LargeKinfuImpl::reset() +{ + frameCounter = 0; + pose = Affine3f::Identity(); + submapMgr->reset(); +} + +template +LargeKinfuImpl::~LargeKinfuImpl() +{ +} + +template +const Params& LargeKinfuImpl::getParams() const +{ + return params; +} + +template +const Affine3f LargeKinfuImpl::getPose() const +{ + return pose; +} + +template<> +bool LargeKinfuImpl::update(InputArray _depth) +{ + CV_Assert(!_depth.empty() && _depth.size() == params.frameSize); + + Mat depth; + if (_depth.isUMat()) + { + _depth.copyTo(depth); + return updateT(depth); + } + else + { + return updateT(_depth.getMat()); + } +} + +template<> +bool LargeKinfuImpl::update(InputArray _depth) +{ + CV_Assert(!_depth.empty() && _depth.size() == params.frameSize); + + UMat depth; + if (!_depth.isUMat()) + { + _depth.copyTo(depth); + return updateT(depth); + } + else + { + return updateT(_depth.getUMat()); + } +} + +template +bool LargeKinfuImpl::updateT(const MatType& _depth) +{ + CV_TRACE_FUNCTION(); + + MatType depth; + if (_depth.type() != DEPTH_TYPE) + _depth.convertTo(depth, DEPTH_TYPE); + else + depth = _depth; + + std::vector newPoints, newNormals; + makeFrameFromDepth(depth, newPoints, newNormals, params.intr, params.pyramidLevels, params.depthFactor, + params.bilateral_sigma_depth, params.bilateral_sigma_spatial, params.bilateral_kernel_size, + params.truncateThreshold); + + std::cout << "Current frameID: " << frameCounter << "\n"; + for (const auto& it : submapMgr->activeSubmaps) + { + int currTrackingId = it.first; + auto submapData = it.second; + Ptr> currTrackingSubmap = submapMgr->getSubmap(currTrackingId); + Affine3f affine; + std::cout << "Current tracking ID: " << currTrackingId << std::endl; + + if(frameCounter == 0) //! Only one current tracking map + { + currTrackingSubmap->integrate(depth, params.depthFactor, params.intr, frameCounter); + currTrackingSubmap->pyrPoints = newPoints; + currTrackingSubmap->pyrNormals = newNormals; + continue; + } + + //1. Track + bool trackingSuccess = + icp->estimateTransform(affine, currTrackingSubmap->pyrPoints, currTrackingSubmap->pyrNormals, newPoints, newNormals); + if (trackingSuccess) + currTrackingSubmap->composeCameraPose(affine); + else + { + std::cout << "Tracking failed" << std::endl; + continue; + } + + //2. Integrate + if(submapData.type == SubmapManager::Type::NEW || submapData.type == SubmapManager::Type::CURRENT) + { + float rnorm = (float)cv::norm(affine.rvec()); + float tnorm = (float)cv::norm(affine.translation()); + // We do not integrate volume if camera does not move + if ((rnorm + tnorm) / 2 >= params.tsdf_min_camera_movement) + currTrackingSubmap->integrate(depth, params.depthFactor, params.intr, frameCounter); + } + + //3. Raycast + currTrackingSubmap->raycast(currTrackingSubmap->cameraPose, params.intr, params.frameSize, currTrackingSubmap->pyrPoints[0], currTrackingSubmap->pyrNormals[0]); + + currTrackingSubmap->updatePyrPointsNormals(params.pyramidLevels); + + std::cout << "Submap: " << currTrackingId << " Total allocated blocks: " << currTrackingSubmap->getTotalAllocatedBlocks() << "\n"; + std::cout << "Submap: " << currTrackingId << " Visible blocks: " << currTrackingSubmap->getVisibleBlocks(frameCounter) << "\n"; + + } + //4. Update map + bool isMapUpdated = submapMgr->updateMap(frameCounter, newPoints, newNormals); + + if(isMapUpdated) + { + // TODO: Convert constraints to posegraph + PoseGraph poseGraph = submapMgr->MapToPoseGraph(); + std::cout << "Created posegraph\n"; + Optimizer::optimize(poseGraph); + submapMgr->PoseGraphToMap(poseGraph); + + } + std::cout << "Number of submaps: " << submapMgr->submapList.size() << "\n"; + + frameCounter++; + return true; +} + +template +void LargeKinfuImpl::render(OutputArray image, const Matx44f& _cameraPose) const +{ + CV_TRACE_FUNCTION(); + + Affine3f cameraPose(_cameraPose); + + auto currSubmap = submapMgr->getCurrentSubmap(); + const Affine3f id = Affine3f::Identity(); + if ((cameraPose.rotation() == pose.rotation() && cameraPose.translation() == pose.translation()) || + (cameraPose.rotation() == id.rotation() && cameraPose.translation() == id.translation())) + { + //! TODO: Can render be dependent on current submap + renderPointsNormals(currSubmap->pyrPoints[0], currSubmap->pyrNormals[0], image, params.lightPose); + } + else + { + MatType points, normals; + currSubmap->raycast(cameraPose, params.intr, params.frameSize, points, normals); + renderPointsNormals(points, normals, image, params.lightPose); + } +} + +template +void LargeKinfuImpl::getCloud(OutputArray p, OutputArray n) const +{ + auto currSubmap = submapMgr->getCurrentSubmap(); + currSubmap->volume.fetchPointsNormals(p, n); +} + +template +void LargeKinfuImpl::getPoints(OutputArray points) const +{ + auto currSubmap = submapMgr->getCurrentSubmap(); + currSubmap->volume.fetchPointsNormals(points, noArray()); +} + +template +void LargeKinfuImpl::getNormals(InputArray points, OutputArray normals) const +{ + auto currSubmap = submapMgr->getCurrentSubmap(); + currSubmap->volume.fetchNormals(points, normals); +} + +// importing class + +#ifdef OPENCV_ENABLE_NONFREE + +Ptr LargeKinfu::create(const Ptr& params) +{ + CV_Assert((int)params->icpIterations.size() == params->pyramidLevels); + CV_Assert(params->intr(0, 1) == 0 && params->intr(1, 0) == 0 && params->intr(2, 0) == 0 && params->intr(2, 1) == 0 && + params->intr(2, 2) == 1); +#ifdef HAVE_OPENCL + if (cv::ocl::useOpenCL()) + return makePtr>(*params); +#endif + return makePtr>(*params); +} + +#else +Ptr LargeKinfu::create(const Ptr& /* params */) +{ + CV_Error(Error::StsNotImplemented, + "This algorithm is patented and is excluded in this configuration; " + "Set OPENCV_ENABLE_NONFREE CMake option and rebuild the library"); +} +#endif +} // namespace large_kinfu +} // namespace cv diff --git a/modules/rgbd/src/pose_graph.cpp b/modules/rgbd/src/pose_graph.cpp new file mode 100644 index 00000000000..b525e455129 --- /dev/null +++ b/modules/rgbd/src/pose_graph.cpp @@ -0,0 +1,169 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +#include "pose_graph.hpp" + +#include +#include +#include +#include + +#if defined(CERES_FOUND) +#include +#endif + +namespace cv +{ +namespace kinfu +{ +bool PoseGraph::isValid() const +{ + int numNodes = getNumNodes(); + int numEdges = getNumEdges(); + + if (numNodes <= 0 || numEdges <= 0) + return false; + + std::unordered_set nodesVisited; + std::vector nodesToVisit; + + nodesToVisit.push_back(nodes.at(0).getId()); + + bool isGraphConnected = false; + while (!nodesToVisit.empty()) + { + int currNodeId = nodesToVisit.back(); + nodesToVisit.pop_back(); + std::cout << "Visiting node: " << currNodeId << "\n"; + nodesVisited.insert(currNodeId); + // Since each node does not maintain its neighbor list + for (int i = 0; i < numEdges; i++) + { + const PoseGraphEdge& potentialEdge = edges.at(i); + int nextNodeId = -1; + + if (potentialEdge.getSourceNodeId() == currNodeId) + { + nextNodeId = potentialEdge.getTargetNodeId(); + } + else if (potentialEdge.getTargetNodeId() == currNodeId) + { + nextNodeId = potentialEdge.getSourceNodeId(); + } + if (nextNodeId != -1) + { + std::cout << "Next node: " << nextNodeId << " " << nodesVisited.count(nextNodeId) + << std::endl; + if (nodesVisited.count(nextNodeId) == 0) + { + nodesToVisit.push_back(nextNodeId); + } + } + } + } + + isGraphConnected = (int(nodesVisited.size()) == numNodes); + std::cout << "nodesVisited: " << nodesVisited.size() + << " IsGraphConnected: " << isGraphConnected << std::endl; + bool invalidEdgeNode = false; + for (int i = 0; i < numEdges; i++) + { + const PoseGraphEdge& edge = edges.at(i); + // edges have spurious source/target nodes + if ((nodesVisited.count(edge.getSourceNodeId()) != 1) || + (nodesVisited.count(edge.getTargetNodeId()) != 1)) + { + invalidEdgeNode = true; + break; + } + } + return isGraphConnected && !invalidEdgeNode; +} + +#if defined(CERES_FOUND) && defined(HAVE_EIGEN) +void Optimizer::createOptimizationProblem(PoseGraph& poseGraph, ceres::Problem& problem) +{ + int numEdges = poseGraph.getNumEdges(); + int numNodes = poseGraph.getNumNodes(); + if (numEdges == 0) + { + CV_Error(Error::StsBadArg, "PoseGraph has no edges, no optimization to be done"); + return; + } + + ceres::LossFunction* lossFunction = nullptr; + // TODO: Experiment with SE3 parameterization + ceres::LocalParameterization* quatLocalParameterization = + new ceres::EigenQuaternionParameterization; + + for (int currEdgeNum = 0; currEdgeNum < numEdges; ++currEdgeNum) + { + const PoseGraphEdge& currEdge = poseGraph.edges.at(currEdgeNum); + int sourceNodeId = currEdge.getSourceNodeId(); + int targetNodeId = currEdge.getTargetNodeId(); + Pose3d& sourcePose = poseGraph.nodes.at(sourceNodeId).se3Pose; + Pose3d& targetPose = poseGraph.nodes.at(targetNodeId).se3Pose; + + const Matx66f& informationMatrix = currEdge.information; + + ceres::CostFunction* costFunction = Pose3dErrorFunctor::create( + Pose3d(currEdge.transformation.rotation(), currEdge.transformation.translation()), + informationMatrix); + + problem.AddResidualBlock(costFunction, lossFunction, sourcePose.t.data(), + sourcePose.r.coeffs().data(), targetPose.t.data(), + targetPose.r.coeffs().data()); + problem.SetParameterization(sourcePose.r.coeffs().data(), quatLocalParameterization); + problem.SetParameterization(targetPose.r.coeffs().data(), quatLocalParameterization); + } + + for (int currNodeId = 0; currNodeId < numNodes; ++currNodeId) + { + PoseGraphNode& currNode = poseGraph.nodes.at(currNodeId); + if (currNode.isPoseFixed()) + { + problem.SetParameterBlockConstant(currNode.se3Pose.t.data()); + problem.SetParameterBlockConstant(currNode.se3Pose.r.coeffs().data()); + } + } +} +#endif + +void Optimizer::optimize(PoseGraph& poseGraph) +{ + PoseGraph poseGraphOriginal = poseGraph; + + if (!poseGraphOriginal.isValid()) + { + CV_Error(Error::StsBadArg, + "Invalid PoseGraph that is either not connected or has invalid nodes"); + return; + } + + int numNodes = poseGraph.getNumNodes(); + int numEdges = poseGraph.getNumEdges(); + std::cout << "Optimizing PoseGraph with " << numNodes << " nodes and " << numEdges << " edges" + << std::endl; + +#if defined(CERES_FOUND) && defined(HAVE_EIGEN) + ceres::Problem problem; + createOptimizationProblem(poseGraph, problem); + + ceres::Solver::Options options; + options.max_num_iterations = 100; + options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY; + + ceres::Solver::Summary summary; + ceres::Solve(options, &problem, &summary); + + std::cout << summary.FullReport() << '\n'; + + std::cout << "Is solution usable: " << summary.IsSolutionUsable() << std::endl; +#else + CV_Error(Error::StsNotImplemented, "Ceres and Eigen required for Pose Graph optimization"); +#endif +} + +} // namespace kinfu +} // namespace cv diff --git a/modules/rgbd/src/pose_graph.hpp b/modules/rgbd/src/pose_graph.hpp new file mode 100644 index 00000000000..bb0164723be --- /dev/null +++ b/modules/rgbd/src/pose_graph.hpp @@ -0,0 +1,321 @@ +#ifndef OPENCV_RGBD_GRAPH_NODE_H +#define OPENCV_RGBD_GRAPH_NODE_H + +#include +#include + +#include "opencv2/core/affine.hpp" +#if defined(HAVE_EIGEN) +#include +#include +#include "opencv2/core/eigen.hpp" +#endif + +#if defined(CERES_FOUND) +#include +#endif + +namespace cv +{ +namespace kinfu +{ +/*! \class GraphNode + * \brief Defines a node/variable that is optimizable in a posegraph + * + * Detailed description + */ +#if defined(HAVE_EIGEN) +struct Pose3d +{ + EIGEN_MAKE_ALIGNED_OPERATOR_NEW + + Eigen::Vector3d t; + Eigen::Quaterniond r; + + Pose3d() + { + t.setZero(); + r.setIdentity(); + }; + Pose3d(const Eigen::Matrix3d& rotation, const Eigen::Vector3d& translation) + : t(translation), r(Eigen::Quaterniond(rotation)) + { + normalizeRotation(); + } + + Pose3d(const Matx33d& rotation, const Vec3d& translation) + { + Eigen::Matrix3d R; + cv2eigen(rotation, R); + cv2eigen(translation, t); + r = Eigen::Quaterniond(R); + normalizeRotation(); + } + + explicit Pose3d(const Matx44f& pose) + { + Matx33d rotation(pose.val[0], pose.val[1], pose.val[2], pose.val[4], pose.val[5], + pose.val[6], pose.val[8], pose.val[9], pose.val[10]); + Vec3d translation(pose.val[3], pose.val[7], pose.val[11]); + Pose3d(rotation, translation); + } + + // NOTE: Eigen overloads quaternion multiplication appropriately + inline Pose3d operator*(const Pose3d& otherPose) const + { + Pose3d out(*this); + out.t += r * otherPose.t; + out.r *= otherPose.r; + out.normalizeRotation(); + return out; + } + + inline Pose3d& operator*=(const Pose3d& otherPose) + { + t += otherPose.t; + r *= otherPose.r; + normalizeRotation(); + return *this; + } + + inline Pose3d inverse() const + { + Pose3d out; + out.r = r.conjugate(); + out.t = out.r * (t * -1.0); + return out; + } + + inline void normalizeRotation() + { + if (r.w() < 0) + r.coeffs() *= -1.0; + r.normalize(); + } +}; +#endif + +struct PoseGraphNode +{ + public: + explicit PoseGraphNode(int _nodeId, const Affine3f& _pose) + : nodeId(_nodeId), isFixed(false), pose(_pose) + { +#if defined(HAVE_EIGEN) + se3Pose = Pose3d(_pose.rotation(), _pose.translation()); +#endif + } + virtual ~PoseGraphNode() = default; + + int getId() const { return nodeId; } + inline Affine3f getPose() const + { + return pose; + } + void setPose(const Affine3f& _pose) + { + pose = _pose; +#if defined(HAVE_EIGEN) + se3Pose = Pose3d(pose.rotation(), pose.translation()); +#endif + } +#if defined(HAVE_EIGEN) + void setPose(const Pose3d& _pose) + { + se3Pose = _pose; + const Eigen::Matrix3d& rotation = se3Pose.r.toRotationMatrix(); + const Eigen::Vector3d& translation = se3Pose.t; + Matx33d rot; + Vec3d trans; + eigen2cv(rotation, rot); + eigen2cv(translation, trans); + Affine3d poseMatrix(rot, trans); + pose = poseMatrix; + } +#endif + void setFixed(bool val = true) { isFixed = val; } + bool isPoseFixed() const { return isFixed; } + + public: + int nodeId; + bool isFixed; + Affine3f pose; +#if defined(HAVE_EIGEN) + Pose3d se3Pose; +#endif +}; + +/*! \class PoseGraphEdge + * \brief Defines the constraints between two PoseGraphNodes + * + * Detailed description + */ +struct PoseGraphEdge +{ + public: + PoseGraphEdge(int _sourceNodeId, int _targetNodeId, const Affine3f& _transformation, + const Matx66f& _information = Matx66f::eye()) + : sourceNodeId(_sourceNodeId), + targetNodeId(_targetNodeId), + transformation(_transformation), + information(_information) + { + } + virtual ~PoseGraphEdge() = default; + + int getSourceNodeId() const { return sourceNodeId; } + int getTargetNodeId() const { return targetNodeId; } + + bool operator==(const PoseGraphEdge& edge) + { + if ((edge.getSourceNodeId() == sourceNodeId && edge.getTargetNodeId() == targetNodeId) || + (edge.getSourceNodeId() == targetNodeId && edge.getTargetNodeId() == sourceNodeId)) + return true; + return false; + } + + public: + int sourceNodeId; + int targetNodeId; + Affine3f transformation; + Matx66f information; +}; + +//! @brief Reference: A tutorial on SE(3) transformation parameterizations and on-manifold +//! optimization Jose Luis Blanco Compactly represents the jacobian of the SE3 generator +// clang-format off +/* static const std::array generatorJacobian = { */ +/* // alpha */ +/* Matx44f(0, 0, 0, 0, */ +/* 0, 0, -1, 0, */ +/* 0, 1, 0, 0, */ +/* 0, 0, 0, 0), */ +/* // beta */ +/* Matx44f( 0, 0, 1, 0, */ +/* 0, 0, 0, 0, */ +/* -1, 0, 0, 0, */ +/* 0, 0, 0, 0), */ +/* // gamma */ +/* Matx44f(0, -1, 0, 0, */ +/* 1, 0, 0, 0, */ +/* 0, 0, 0, 0, */ +/* 0, 0, 0, 0), */ +/* // x */ +/* Matx44f(0, 0, 0, 1, */ +/* 0, 0, 0, 0, */ +/* 0, 0, 0, 0, */ +/* 0, 0, 0, 0), */ +/* // y */ +/* Matx44f(0, 0, 0, 0, */ +/* 0, 0, 0, 1, */ +/* 0, 0, 0, 0, */ +/* 0, 0, 0, 0), */ +/* // z */ +/* Matx44f(0, 0, 0, 0, */ +/* 0, 0, 0, 0, */ +/* 0, 0, 0, 1, */ +/* 0, 0, 0, 0) */ +/* }; */ +// clang-format on + +class PoseGraph +{ + public: + typedef std::vector NodeVector; + typedef std::vector EdgeVector; + + explicit PoseGraph(){}; + virtual ~PoseGraph() = default; + + //! PoseGraph can be copied/cloned + PoseGraph(const PoseGraph& _poseGraph) = default; + PoseGraph& operator=(const PoseGraph& _poseGraph) = default; + + void addNode(const PoseGraphNode& node) { nodes.push_back(node); } + void addEdge(const PoseGraphEdge& edge) { edges.push_back(edge); } + + bool nodeExists(int nodeId) const + { + return std::find_if(nodes.begin(), nodes.end(), [nodeId](const PoseGraphNode& currNode) { + return currNode.getId() == nodeId; + }) != nodes.end(); + } + + bool isValid() const; + + int getNumNodes() const { return int(nodes.size()); } + int getNumEdges() const { return int(edges.size()); } + + public: + NodeVector nodes; + EdgeVector edges; +}; + +namespace Optimizer +{ +void optimize(PoseGraph& poseGraph); + +#if defined(CERES_FOUND) +void createOptimizationProblem(PoseGraph& poseGraph, ceres::Problem& problem); + +//! Error Functor required for Ceres to obtain an auto differentiable cost function +class Pose3dErrorFunctor +{ + public: + EIGEN_MAKE_ALIGNED_OPERATOR_NEW + Pose3dErrorFunctor(const Pose3d& _poseMeasurement, const Matx66d& _sqrtInformation) + : poseMeasurement(_poseMeasurement) + { + cv2eigen(_sqrtInformation, sqrtInfo); + } + Pose3dErrorFunctor(const Pose3d& _poseMeasurement, + const Eigen::Matrix& _sqrtInformation) + : poseMeasurement(_poseMeasurement), sqrtInfo(_sqrtInformation) + { + } + + template + bool operator()(const T* const _pSourceTrans, const T* const _pSourceQuat, + const T* const _pTargetTrans, const T* const _pTargetQuat, T* _pResidual) const + { + Eigen::Map> sourceTrans(_pSourceTrans); + Eigen::Map> targetTrans(_pTargetTrans); + Eigen::Map> sourceQuat(_pSourceQuat); + Eigen::Map> targetQuat(_pTargetQuat); + Eigen::Map> residual(_pResidual); + + Eigen::Quaternion targetQuatInv = targetQuat.conjugate(); + + Eigen::Quaternion relativeQuat = targetQuatInv * sourceQuat; + Eigen::Matrix relativeTrans = targetQuatInv * (targetTrans - sourceTrans); + + //! Definition should actually be relativeQuat * poseMeasurement.r.conjugate() + Eigen::Quaternion deltaRot = + poseMeasurement.r.template cast() * relativeQuat.conjugate(); + + residual.template block<3, 1>(0, 0) = relativeTrans - poseMeasurement.t.template cast(); + residual.template block<3, 1>(3, 0) = T(2.0) * deltaRot.vec(); + + residual.applyOnTheLeft(sqrtInfo.template cast()); + + return true; + } + + static ceres::CostFunction* create(const Pose3d& _poseMeasurement, + const Matx66f& _sqrtInformation) + { + return new ceres::AutoDiffCostFunction( + new Pose3dErrorFunctor(_poseMeasurement, _sqrtInformation)); + } + + private: + const Pose3d poseMeasurement; + Eigen::Matrix sqrtInfo; +}; +#endif + +} // namespace Optimizer + +} // namespace kinfu +} // namespace cv +#endif /* ifndef OPENCV_RGBD_GRAPH_NODE_H */ diff --git a/modules/rgbd/src/sparse_block_matrix.hpp b/modules/rgbd/src/sparse_block_matrix.hpp new file mode 100644 index 00000000000..0e607af639d --- /dev/null +++ b/modules/rgbd/src/sparse_block_matrix.hpp @@ -0,0 +1,159 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +#include +#include + +#include "opencv2/core/base.hpp" +#include "opencv2/core/types.hpp" + +#if defined(HAVE_EIGEN) +#include +#include +#include + +#include "opencv2/core/eigen.hpp" +#endif + +namespace cv +{ +namespace kinfu +{ +/*! + * \class BlockSparseMat + * Naive implementation of Sparse Block Matrix + */ +template +struct BlockSparseMat +{ + struct Point2iHash + { + size_t operator()(const cv::Point2i& point) const noexcept + { + size_t seed = 0; + constexpr uint32_t GOLDEN_RATIO = 0x9e3779b9; + seed ^= std::hash()(point.x) + GOLDEN_RATIO + (seed << 6) + (seed >> 2); + seed ^= std::hash()(point.y) + GOLDEN_RATIO + (seed << 6) + (seed >> 2); + return seed; + } + }; + typedef Matx<_Tp, blockM, blockN> MatType; + typedef std::unordered_map IDtoBlockValueMap; + + BlockSparseMat(int _nBlocks) : nBlocks(_nBlocks), ijValue() {} + + MatType& refBlock(int i, int j) + { + Point2i p(i, j); + auto it = ijValue.find(p); + if (it == ijValue.end()) + { + it = ijValue.insert({ p, Matx<_Tp, blockM, blockN>::zeros() }).first; + } + return it->second; + } + + Mat diagonal() + { + // Diagonal max length is the number of columns in the sparse matrix + int diagLength = blockN * nBlocks; + cv::Mat diag = cv::Mat::zeros(diagLength, 1, CV_32F); + + for (int i = 0; i < diagLength; i++) + { + diag.at(i, 0) = refElem(i, i); + } + return diag; + } + + float& refElem(int i, int j) + { + Point2i ib(i / blockM, j / blockN), iv(i % blockM, j % blockN); + return refBlock(ib.x, ib.y)(iv.x, iv.y); + } + +#if defined(HAVE_EIGEN) + Eigen::SparseMatrix<_Tp> toEigen() const + { + std::vector> tripletList; + tripletList.reserve(ijValue.size() * blockM * blockN); + for (auto ijv : ijValue) + { + int xb = ijv.first.x, yb = ijv.first.y; + MatType vblock = ijv.second; + for (int i = 0; i < blockM; i++) + { + for (int j = 0; j < blockN; j++) + { + float val = vblock(i, j); + if (abs(val) >= NON_ZERO_VAL_THRESHOLD) + { + tripletList.push_back(Eigen::Triplet(blockM * xb + i, blockN * yb + j, val)); + } + } + } + } + Eigen::SparseMatrix<_Tp> EigenMat(blockM * nBlocks, blockN * nBlocks); + EigenMat.setFromTriplets(tripletList.begin(), tripletList.end()); + EigenMat.makeCompressed(); + + return EigenMat; + } +#endif + size_t nonZeroBlocks() const { return ijValue.size(); } + + static constexpr float NON_ZERO_VAL_THRESHOLD = 0.0001f; + int nBlocks; + IDtoBlockValueMap ijValue; +}; + +//! Function to solve a sparse linear system of equations HX = B +//! Requires Eigen +static bool sparseSolve(const BlockSparseMat& H, const Mat& B, Mat& X, Mat& predB) +{ + bool result = false; +#if defined(HAVE_EIGEN) + Eigen::SparseMatrix bigA = H.toEigen(); + Eigen::VectorXf bigB; + cv2eigen(B, bigB); + + Eigen::SparseMatrix bigAtranspose = bigA.transpose(); + if(!bigA.isApprox(bigAtranspose)) + { + CV_Error(Error::StsBadArg, "H matrix is not symmetrical"); + return result; + } + + Eigen::SimplicialLDLT> solver; + + solver.compute(bigA); + if (solver.info() != Eigen::Success) + { + std::cout << "failed to eigen-decompose" << std::endl; + result = false; + } + else + { + Eigen::VectorXf solutionX = solver.solve(bigB); + Eigen::VectorXf predBEigen = bigA * solutionX; + if (solver.info() != Eigen::Success) + { + std::cout << "failed to eigen-solve" << std::endl; + result = false; + } + else + { + eigen2cv(solutionX, X); + eigen2cv(predBEigen, predB); + result = true; + } + } +#else + std::cout << "no eigen library" << std::endl; + CV_Error(Error::StsNotImplemented, "Eigen library required for matrix solve, dense solver is not implemented"); +#endif + return result; +} +} // namespace kinfu +} // namespace cv diff --git a/modules/rgbd/src/submap.hpp b/modules/rgbd/src/submap.hpp new file mode 100644 index 00000000000..983ee610c75 --- /dev/null +++ b/modules/rgbd/src/submap.hpp @@ -0,0 +1,544 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +#ifndef __OPENCV_RGBD_SUBMAP_HPP__ +#define __OPENCV_RGBD_SUBMAP_HPP__ + +#include + +#include +#include +#include + +#include "hash_tsdf.hpp" +#include "opencv2/core/mat.inl.hpp" +#include "pose_graph.hpp" + +namespace cv +{ +namespace kinfu +{ +template +class Submap +{ + public: + struct PoseConstraint + { + Affine3f estimatedPose; + int weight; + + PoseConstraint() : weight(0){}; + + void accumulatePose(const Affine3f& _pose, int _weight = 1) + { + Matx44f accPose = estimatedPose.matrix * weight + _pose.matrix * _weight; + weight += _weight; + accPose /= float(weight); + estimatedPose = Affine3f(accPose); + } + }; + typedef std::map Constraints; + + Submap(int _id, const VolumeParams& volumeParams, const cv::Affine3f& _pose = cv::Affine3f::Identity(), + int _startFrameId = 0) + : id(_id), pose(_pose), cameraPose(Affine3f::Identity()), startFrameId(_startFrameId), volume(volumeParams) + { + std::cout << "Created volume\n"; + } + virtual ~Submap() = default; + + virtual void integrate(InputArray _depth, float depthFactor, const cv::kinfu::Intr& intrinsics, const int currframeId); + virtual void raycast(const cv::Affine3f& cameraPose, const cv::kinfu::Intr& intrinsics, cv::Size frameSize, + OutputArray points, OutputArray normals); + virtual void updatePyrPointsNormals(const int pyramidLevels); + + virtual int getTotalAllocatedBlocks() const { return int(volume.getTotalVolumeUnits()); }; + virtual int getVisibleBlocks(int currFrameId) const + { + return volume.getVisibleBlocks(currFrameId, FRAME_VISIBILITY_THRESHOLD); + } + + float calcVisibilityRatio(int currFrameId) const + { + int allocate_blocks = getTotalAllocatedBlocks(); + int visible_blocks = getVisibleBlocks(currFrameId); + return float(visible_blocks) / float(allocate_blocks); + } + + //! TODO: Possibly useless + virtual void setStartFrameId(int _startFrameId) { startFrameId = _startFrameId; }; + virtual void setStopFrameId(int _stopFrameId) { stopFrameId = _stopFrameId; }; + + void composeCameraPose(const cv::Affine3f& _relativePose) { cameraPose = cameraPose * _relativePose; } + PoseConstraint& getConstraint(const int _id) + { + //! Creates constraints if doesn't exist yet + return constraints[_id]; + } + + public: + const int id; + cv::Affine3f pose; + cv::Affine3f cameraPose; + Constraints constraints; + + int startFrameId; + int stopFrameId; + //! TODO: Should we support submaps for regular volumes? + static constexpr int FRAME_VISIBILITY_THRESHOLD = 5; + + //! TODO: Add support for GPU arrays (UMat) + std::vector pyrPoints; + std::vector pyrNormals; + HashTSDFVolumeCPU volume; +}; + +template + +void Submap::integrate(InputArray _depth, float depthFactor, const cv::kinfu::Intr& intrinsics, + const int currFrameId) +{ + CV_Assert(currFrameId >= startFrameId); + volume.integrate(_depth, depthFactor, cameraPose.matrix, intrinsics, currFrameId); +} + +template +void Submap::raycast(const cv::Affine3f& _cameraPose, const cv::kinfu::Intr& intrinsics, cv::Size frameSize, + OutputArray points, OutputArray normals) +{ + volume.raycast(_cameraPose.matrix, intrinsics, frameSize, points, normals); +} + +template +void Submap::updatePyrPointsNormals(const int pyramidLevels) +{ + MatType& points = pyrPoints[0]; + MatType& normals = pyrNormals[0]; + + buildPyramidPointsNormals(points, normals, pyrPoints, pyrNormals, pyramidLevels); +} + +/** + * @brief: Manages all the created submaps for a particular scene + */ +template +class SubmapManager +{ + public: + enum class Type + { + NEW = 0, + CURRENT = 1, + RELOCALISATION = 2, + LOOP_CLOSURE = 3, + LOST = 4 + }; + + struct ActiveSubmapData + { + Type type; + std::vector constraints; + int trackingAttempts; + }; + typedef Submap SubmapT; + typedef std::map> IdToSubmapPtr; + typedef std::unordered_map IdToActiveSubmaps; + + SubmapManager(const VolumeParams& _volumeParams) : volumeParams(_volumeParams) {} + virtual ~SubmapManager() = default; + + void reset() { submapList.clear(); }; + + bool shouldCreateSubmap(int frameId); + bool shouldChangeCurrSubmap(int _frameId, int toSubmapId); + + //! Adds a new submap/volume into the current list of managed/Active submaps + int createNewSubmap(bool isCurrentActiveMap, const int currFrameId = 0, const Affine3f& pose = cv::Affine3f::Identity()); + + void removeSubmap(int _id); + size_t numOfSubmaps(void) const { return submapList.size(); }; + size_t numOfActiveSubmaps(void) const { return activeSubmaps.size(); }; + + Ptr getSubmap(int _id) const; + Ptr getCurrentSubmap(void) const; + + int estimateConstraint(int fromSubmapId, int toSubmapId, int& inliers, Affine3f& inlierPose); + bool updateMap(int _frameId, std::vector _framePoints, std::vector _frameNormals); + + PoseGraph MapToPoseGraph(); + void PoseGraphToMap(const PoseGraph& updatedPoseGraph); + + VolumeParams volumeParams; + + std::vector> submapList; + IdToActiveSubmaps activeSubmaps; + + PoseGraph poseGraph; +}; + +template +int SubmapManager::createNewSubmap(bool isCurrentMap, int currFrameId, const Affine3f& pose) +{ + int newId = int(submapList.size()); + + Ptr newSubmap = cv::makePtr(newId, volumeParams, pose, currFrameId); + submapList.push_back(newSubmap); + + ActiveSubmapData newSubmapData; + newSubmapData.trackingAttempts = 0; + newSubmapData.type = isCurrentMap ? Type::CURRENT : Type::NEW; + activeSubmaps[newId] = newSubmapData; + + std::cout << "Created new submap\n"; + + return newId; +} + +template +Ptr> SubmapManager::getSubmap(int _id) const +{ + CV_Assert(submapList.size() > 0); + CV_Assert(_id >= 0 && _id < int(submapList.size())); + return submapList.at(_id); +} + +template +Ptr> SubmapManager::getCurrentSubmap(void) const +{ + for (const auto& it : activeSubmaps) + { + if (it.second.type == Type::CURRENT) + return getSubmap(it.first); + } + return nullptr; +} + +template +bool SubmapManager::shouldCreateSubmap(int currFrameId) +{ + int currSubmapId = -1; + for (const auto& it : activeSubmaps) + { + auto submapData = it.second; + // No more than 1 new submap at a time! + if (submapData.type == Type::NEW) + { + return false; + } + if (submapData.type == Type::CURRENT) + { + currSubmapId = it.first; + } + } + //! TODO: This shouldn't be happening? since there should always be one active current submap + if (currSubmapId < 0) + { + return false; + } + + Ptr currSubmap = getSubmap(currSubmapId); + float ratio = currSubmap->calcVisibilityRatio(currFrameId); + + std::cout << "Ratio: " << ratio << "\n"; + + if (ratio < 0.2f) + return true; + return false; +} + +template +int SubmapManager::estimateConstraint(int fromSubmapId, int toSubmapId, int& inliers, Affine3f& inlierPose) +{ + static constexpr int MAX_ITER = 10; + static constexpr float CONVERGE_WEIGHT_THRESHOLD = 0.01f; + static constexpr float INLIER_WEIGHT_THRESH = 0.8f; + static constexpr int MIN_INLIERS = 10; + static constexpr int MAX_TRACKING_ATTEMPTS = 25; + + //! thresh = HUBER_THRESH + auto huberWeight = [](float residual, float thresh = 0.1f) -> float { + float rAbs = abs(residual); + if (rAbs < thresh) + return 1.0; + float numerator = sqrt(2 * thresh * rAbs - thresh * thresh); + return numerator / rAbs; + }; + + Ptr fromSubmap = getSubmap(fromSubmapId); + Ptr toSubmap = getSubmap(toSubmapId); + ActiveSubmapData& fromSubmapData = activeSubmaps.at(fromSubmapId); + + Affine3f TcameraToFromSubmap = fromSubmap->cameraPose; + Affine3f TcameraToToSubmap = toSubmap->cameraPose; + + // FromSubmap -> ToSubmap transform + Affine3f candidateConstraint = TcameraToToSubmap * TcameraToFromSubmap.inv(); + fromSubmapData.trackingAttempts++; + fromSubmapData.constraints.push_back(candidateConstraint); + + std::vector weights(fromSubmapData.constraints.size() + 1, 1.0f); + + Affine3f prevConstraint = fromSubmap->getConstraint(toSubmap->id).estimatedPose; + int prevWeight = fromSubmap->getConstraint(toSubmap->id).weight; + + // Iterative reweighted least squares with huber threshold to find the inliers in the past observations + Vec6f meanConstraint; + float sumWeight = 0.0f; + for (int i = 0; i < MAX_ITER; i++) + { + Vec6f constraintVec; + for (int j = 0; j < int(weights.size() - 1); j++) + { + Affine3f currObservation = fromSubmapData.constraints[j]; + cv::vconcat(currObservation.rvec(), currObservation.translation(), constraintVec); + meanConstraint += weights[j] * constraintVec; + sumWeight += weights[j]; + } + // Heavier weight given to the estimatedPose + cv::vconcat(prevConstraint.rvec(), prevConstraint.translation(), constraintVec); + meanConstraint += weights.back() * prevWeight * constraintVec; + sumWeight += prevWeight; + meanConstraint /= float(sumWeight); + + float residual = 0.0f; + float diff = 0.0f; + for (int j = 0; j < int(weights.size()); j++) + { + int w; + if (j == int(weights.size() - 1)) + { + cv::vconcat(prevConstraint.rvec(), prevConstraint.translation(), constraintVec); + w = prevWeight; + } + else + { + Affine3f currObservation = fromSubmapData.constraints[j]; + cv::vconcat(currObservation.rvec(), currObservation.translation(), constraintVec); + w = 1; + } + + cv::Vec6f residualVec = (constraintVec - meanConstraint); + residual = float(norm(residualVec)); + float newWeight = huberWeight(residual); + diff += w * abs(newWeight - weights[j]); + weights[j] = newWeight; + } + + if (diff / (prevWeight + weights.size() - 1) < CONVERGE_WEIGHT_THRESHOLD) + break; + } + + int localInliers = 0; + Matx44f inlierConstraint; + for (int i = 0; i < int(weights.size()); i++) + { + if (weights[i] > INLIER_WEIGHT_THRESH) + { + localInliers++; + if (i == int(weights.size() - 1)) + inlierConstraint += prevConstraint.matrix; + else + inlierConstraint += fromSubmapData.constraints[i].matrix; + } + } + inlierConstraint /= float(max(localInliers, 1)); + inlierPose = Affine3f(inlierConstraint); + inliers = localInliers; + + /* std::cout << inlierPose.matrix << "\n"; */ + /* std::cout << " inliers: " << inliers << "\n"; */ + + if (inliers >= MIN_INLIERS) + { + return 1; + } + if(fromSubmapData.trackingAttempts - inliers > (MAX_TRACKING_ATTEMPTS - MIN_INLIERS)) + { + return -1; + } + + return 0; +} + +template +bool SubmapManager::shouldChangeCurrSubmap(int _frameId, int toSubmapId) +{ + auto toSubmap = getSubmap(toSubmapId); + auto toSubmapData = activeSubmaps.at(toSubmapId); + auto currActiveSubmap = getCurrentSubmap(); + + int blocksInNewMap = toSubmap->getTotalAllocatedBlocks(); + float newRatio = toSubmap->calcVisibilityRatio(_frameId); + + float currRatio = currActiveSubmap->calcVisibilityRatio(_frameId); + + //! TODO: Check for a specific threshold? + if (blocksInNewMap <= 0) + return false; + if ((newRatio > currRatio) && (toSubmapData.type == Type::NEW)) + return true; + + return false; +} + +template +bool SubmapManager::updateMap(int _frameId, std::vector _framePoints, std::vector _frameNormals) +{ + bool mapUpdated = false; + int changedCurrentMapId = -1; + + const int currSubmapId = getCurrentSubmap()->id; + + for (auto& it : activeSubmaps) + { + int submapId = it.first; + auto& submapData = it.second; + if (submapData.type == Type::NEW || submapData.type == Type::LOOP_CLOSURE) + { + // Check with previous estimate + int inliers; + Affine3f inlierPose; + int constraintUpdate = estimateConstraint(submapId, currSubmapId, inliers, inlierPose); + std::cout << "SubmapId: " << submapId << " Tracking attempts: " << submapData.trackingAttempts << "\n"; + if (constraintUpdate == 1) + { + typename SubmapT::PoseConstraint& submapConstraint = getSubmap(submapId)->getConstraint(currSubmapId); + submapConstraint.accumulatePose(inlierPose, inliers); + std::cout << "Submap constraint estimated pose: \n" << submapConstraint.estimatedPose.matrix << "\n"; + submapData.constraints.clear(); + submapData.trackingAttempts = 0; + + if (shouldChangeCurrSubmap(_frameId, submapId)) + { + std::cout << "Should change current map to the new map\n"; + changedCurrentMapId = submapId; + } + mapUpdated = true; + } + else if(constraintUpdate == -1) + { + submapData.type = Type::LOST; + } + } + } + + std::vector createNewConstraintsList; + for (auto& it : activeSubmaps) + { + int submapId = it.first; + auto& submapData = it.second; + + if (submapId == changedCurrentMapId) + { + submapData.type = Type::CURRENT; + } + if ((submapData.type == Type::CURRENT) && (changedCurrentMapId >= 0) && (submapId != changedCurrentMapId)) + { + submapData.type = Type::LOST; + createNewConstraintsList.push_back(submapId); + } + if ((submapData.type == Type::NEW || submapData.type == Type::LOOP_CLOSURE) && (changedCurrentMapId >= 0)) + { + //! TODO: Add a new type called NEW_LOST? + submapData.type = Type::LOST; + createNewConstraintsList.push_back(submapId); + } + } + + for (typename IdToActiveSubmaps::iterator it = activeSubmaps.begin(); it != activeSubmaps.end();) + { + auto& submapData = it->second; + if (submapData.type == Type::LOST) + it = activeSubmaps.erase(it); + else + it++; + } + + for (std::vector::const_iterator it = createNewConstraintsList.begin(); it != createNewConstraintsList.end(); ++it) + { + int dataId = *it; + ActiveSubmapData newSubmapData; + newSubmapData.trackingAttempts = 0; + newSubmapData.type = Type::LOOP_CLOSURE; + activeSubmaps[dataId] = newSubmapData; + } + + if (shouldCreateSubmap(_frameId)) + { + Ptr currActiveSubmap = getCurrentSubmap(); + Affine3f newSubmapPose = currActiveSubmap->pose * currActiveSubmap->cameraPose; + int submapId = createNewSubmap(false, _frameId, newSubmapPose); + auto newSubmap = getSubmap(submapId); + newSubmap->pyrPoints = _framePoints; + newSubmap->pyrNormals = _frameNormals; + } + + // Debugging only + if(_frameId%100 == 0) + { + for(size_t i = 0; i < submapList.size(); i++) + { + Ptr currSubmap = submapList.at(i); + typename SubmapT::Constraints::const_iterator itBegin = currSubmap->constraints.begin(); + std::cout << "Constraint list for SubmapID: " << currSubmap->id << "\n"; + for(typename SubmapT::Constraints::const_iterator it = itBegin; it != currSubmap->constraints.end(); ++it) + { + const typename SubmapT::PoseConstraint& constraint = it->second; + std::cout << "[" << it->first << "] weight: " << constraint.weight << "\n " << constraint.estimatedPose.matrix << " \n"; + } + } + } + + return mapUpdated; +} + +template +PoseGraph SubmapManager::MapToPoseGraph() +{ + PoseGraph localPoseGraph; + + + for(const Ptr currSubmap : submapList) + { + const typename SubmapT::Constraints& constraintList = currSubmap->constraints; + for(const auto& currConstraintPair : constraintList) + { + // TODO: Handle case with duplicate constraints A -> B and B -> A + /* Matx66f informationMatrix = Matx66f::eye() * (currConstraintPair.second.weight/10); */ + Matx66f informationMatrix = Matx66f::eye(); + PoseGraphEdge currEdge(currSubmap->id, currConstraintPair.first, currConstraintPair.second.estimatedPose, informationMatrix); + localPoseGraph.addEdge(currEdge); + } + } + + for(const Ptr currSubmap : submapList) + { + PoseGraphNode currNode(currSubmap->id, currSubmap->pose); + if(currSubmap->id == 0) + { + currNode.setFixed(); + } + localPoseGraph.addNode(currNode); + } + + + + return localPoseGraph; +} + +template +void SubmapManager::PoseGraphToMap(const PoseGraph &updatedPoseGraph) +{ + for(const Ptr currSubmap : submapList) + { + const PoseGraphNode& currNode = updatedPoseGraph.nodes.at(currSubmap->id); + if(!currNode.isPoseFixed()) + currSubmap->pose = currNode.getPose(); + std::cout << "Current node: " << currSubmap->id << " Updated Pose: \n" << currSubmap->pose.matrix << std::endl; + } +} + +} // namespace kinfu +} // namespace cv +#endif /* ifndef __OPENCV_RGBD_SUBMAP_HPP__ */ diff --git a/modules/rgbd/src/tsdf.cpp b/modules/rgbd/src/tsdf.cpp index 947428065d1..ff2a42f87fd 100644 --- a/modules/rgbd/src/tsdf.cpp +++ b/modules/rgbd/src/tsdf.cpp @@ -98,7 +98,7 @@ void TSDFVolumeCPU::reset() }); } -TsdfVoxel TSDFVolumeCPU::at(const cv::Vec3i& volumeIdx) const +TsdfVoxel TSDFVolumeCPU::at(const Vec3i& volumeIdx) const { //! Out of bounds if ((volumeIdx[0] >= volResolution.x || volumeIdx[0] < 0) || @@ -121,7 +121,7 @@ TsdfVoxel TSDFVolumeCPU::at(const cv::Vec3i& volumeIdx) const #if !USE_INTRINSICS static const bool fixMissingData = false; -static inline depthType bilinearDepth(const Depth& m, cv::Point2f pt) +static inline depthType bilinearDepth(const Depth& m, Point2f pt) { const depthType defaultValue = qnan; if(pt.x < 0 || pt.x >= m.cols-1 || @@ -455,7 +455,7 @@ struct IntegrateInvoker : ParallelLoopBody const Depth& depth; const Intr& intr; const Intr::Projector proj; - const cv::Affine3f vol2cam; + const Affine3f vol2cam; const float truncDistInv; const float dfac; TsdfVoxel* volDataStart; @@ -487,11 +487,11 @@ static cv::Mat preCalculationPixNorm(Depth depth, const Intr& intrinsics) } // use depth instead of distance (optimization) -void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, - const Intr& intrinsics) +void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, + const Intr& intrinsics, const int frameId) { CV_TRACE_FUNCTION(); - + CV_UNUSED(frameId); CV_Assert(_depth.type() == DEPTH_TYPE); CV_Assert(!_depth.empty()); Depth depth = _depth.getMat(); @@ -513,7 +513,7 @@ void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const cv::Ma #if USE_INTRINSICS // all coordinate checks should be done in inclosing cycle -inline float TSDFVolumeCPU::interpolateVoxel(Point3f _p) const +inline float TSDFVolumeCPU::interpolateVoxel(const Point3f& _p) const { v_float32x4 p(_p.x, _p.y, _p.z, 0); return interpolateVoxel(p); @@ -560,7 +560,7 @@ inline float TSDFVolumeCPU::interpolateVoxel(const v_float32x4& p) const return v0 + tx*(v1 - v0); } #else -inline float TSDFVolumeCPU::interpolateVoxel(Point3f p) const +inline float TSDFVolumeCPU::interpolateVoxel(const Point3f& p) const { int xdim = volDims[0], ydim = volDims[1], zdim = volDims[2]; @@ -594,7 +594,7 @@ inline float TSDFVolumeCPU::interpolateVoxel(Point3f p) const #if USE_INTRINSICS //gradientDeltaFactor is fixed at 1.0 of voxel size -inline Point3f TSDFVolumeCPU::getNormalVoxel(Point3f _p) const +inline Point3f TSDFVolumeCPU::getNormalVoxel(const Point3f& _p) const { v_float32x4 p(_p.x, _p.y, _p.z, 0.f); v_float32x4 result = getNormalVoxel(p); @@ -662,7 +662,7 @@ inline v_float32x4 TSDFVolumeCPU::getNormalVoxel(const v_float32x4& p) const return Norm.get0() < 0.0001f ? nanv : n/Norm; } #else -inline Point3f TSDFVolumeCPU::getNormalVoxel(Point3f p) const +inline Point3f TSDFVolumeCPU::getNormalVoxel(const Point3f& p) const { const int xdim = volDims[0], ydim = volDims[1], zdim = volDims[2]; const TsdfVoxel* volData = volume.ptr(); @@ -994,8 +994,8 @@ struct RaycastInvoker : ParallelLoopBody }; -void TSDFVolumeCPU::raycast(const cv::Matx44f& cameraPose, const Intr& intrinsics, Size frameSize, - cv::OutputArray _points, cv::OutputArray _normals) const +void TSDFVolumeCPU::raycast(const Matx44f& cameraPose, const Intr& intrinsics, const Size& frameSize, + OutputArray _points, OutputArray _normals) const { CV_TRACE_FUNCTION(); @@ -1189,7 +1189,7 @@ void TSDFVolumeCPU::fetchNormals(InputArray _points, OutputArray _normals) const ///////// GPU implementation ///////// #ifdef HAVE_OPENCL -TSDFVolumeGPU::TSDFVolumeGPU(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, +TSDFVolumeGPU::TSDFVolumeGPU(float _voxelSize, Matx44f _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, Point3i _resolution) : TSDFVolume(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, _resolution, false) { @@ -1251,24 +1251,25 @@ static cv::UMat preCalculationPixNormGPU(int depth_rows, int depth_cols, Vec2f f // use depth instead of distance (optimization) void TSDFVolumeGPU::integrate(InputArray _depth, float depthFactor, - const cv::Matx44f& cameraPose, const Intr& intrinsics) + const Matx44f& cameraPose, const Intr& intrinsics, const int frameId) { CV_TRACE_FUNCTION(); + CV_UNUSED(frameId); CV_Assert(!_depth.empty()); UMat depth = _depth.getUMat(); - cv::String errorStr; - cv::String name = "integrate"; + String errorStr; + String name = "integrate"; ocl::ProgramSource source = ocl::rgbd::tsdf_oclsrc; - cv::String options = "-cl-mad-enable"; + String options = "-cl-mad-enable"; ocl::Kernel k; k.create(name.c_str(), source, options, &errorStr); if(k.empty()) throw std::runtime_error("Failed to create kernel: " + errorStr); - cv::Affine3f vol2cam(Affine3f(cameraPose.inv()) * pose); + Affine3f vol2cam(Affine3f(cameraPose.inv()) * pose); float dfac = 1.f/depthFactor; Vec4i volResGpu(volResolution.x, volResolution.y, volResolution.z); Vec2f fxy(intrinsics.fx, intrinsics.fy), cxy(intrinsics.cx, intrinsics.cy); @@ -1308,17 +1309,17 @@ void TSDFVolumeGPU::integrate(InputArray _depth, float depthFactor, } -void TSDFVolumeGPU::raycast(const cv::Matx44f& cameraPose, const Intr& intrinsics, Size frameSize, - cv::OutputArray _points, cv::OutputArray _normals) const +void TSDFVolumeGPU::raycast(const Matx44f& cameraPose, const Intr& intrinsics, const Size& frameSize, + OutputArray _points, OutputArray _normals) const { CV_TRACE_FUNCTION(); CV_Assert(frameSize.area() > 0); - cv::String errorStr; - cv::String name = "raycast"; + String errorStr; + String name = "raycast"; ocl::ProgramSource source = ocl::rgbd::tsdf_oclsrc; - cv::String options = "-cl-mad-enable"; + String options = "-cl-mad-enable"; ocl::Kernel k; k.create(name.c_str(), source, options, &errorStr); @@ -1384,10 +1385,10 @@ void TSDFVolumeGPU::fetchNormals(InputArray _points, OutputArray _normals) const _normals.createSameSize(_points, POINT_TYPE); UMat normals = _normals.getUMat(); - cv::String errorStr; - cv::String name = "getNormals"; + String errorStr; + String name = "getNormals"; ocl::ProgramSource source = ocl::rgbd::tsdf_oclsrc; - cv::String options = "-cl-mad-enable"; + String options = "-cl-mad-enable"; ocl::Kernel k; k.create(name.c_str(), source, options, &errorStr); @@ -1432,9 +1433,9 @@ void TSDFVolumeGPU::fetchPointsNormals(OutputArray points, OutputArray normals) ocl::Kernel kscan; - cv::String errorStr; + String errorStr; ocl::ProgramSource source = ocl::rgbd::tsdf_oclsrc; - cv::String options = "-cl-mad-enable"; + String options = "-cl-mad-enable"; kscan.create("scanSize", source, options, &errorStr); @@ -1485,7 +1486,7 @@ void TSDFVolumeGPU::fetchPointsNormals(OutputArray points, OutputArray normals) throw std::runtime_error("Failed to run kernel"); Mat groupedSumCpu = groupedSum.getMat(ACCESS_READ); - int gpuSum = (int)cv::sum(groupedSumCpu)[0]; + int gpuSum = (int)sum(groupedSumCpu)[0]; // should be no CPU copies when new kernel is executing groupedSumCpu.release(); @@ -1541,16 +1542,28 @@ void TSDFVolumeGPU::fetchPointsNormals(OutputArray points, OutputArray normals) #endif -cv::Ptr makeTSDFVolume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, +Ptr makeTSDFVolume(float _voxelSize, Matx44f _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, Point3i _resolution) { #ifdef HAVE_OPENCL - if (cv::ocl::useOpenCL()) - return cv::makePtr(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, - _resolution); -#endif - return cv::makePtr(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, + if (ocl::useOpenCL()) + return makePtr(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, _resolution); +#endif + return makePtr(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, + _resolution); +} + +Ptr makeTSDFVolume(const VolumeParams& _params) +{ +#ifdef HAVE_OPENCL + if (ocl::useOpenCL()) + return makePtr(_params.voxelSize, _params.pose.matrix, _params.raycastStepFactor, + _params.tsdfTruncDist, _params.maxWeight, _params.resolution); +#endif + return makePtr(_params.voxelSize, _params.pose.matrix, _params.raycastStepFactor, + _params.tsdfTruncDist, _params.maxWeight, _params.resolution); + } } // namespace kinfu diff --git a/modules/rgbd/src/tsdf.hpp b/modules/rgbd/src/tsdf.hpp index a4017c73a37..cd38ee80b69 100644 --- a/modules/rgbd/src/tsdf.hpp +++ b/modules/rgbd/src/tsdf.hpp @@ -2,7 +2,8 @@ // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html -// This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this module's directory +// This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this +// module's directory #ifndef __OPENCV_KINFU_TSDF_H__ #define __OPENCV_KINFU_TSDF_H__ @@ -35,7 +36,7 @@ class TSDFVolume : public Volume { public: // dimension in voxels, size in meters - TSDFVolume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, float _truncDist, + TSDFVolume(float _voxelSize, Matx44f _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, Point3i _resolution, bool zFirstMemOrder = true); virtual ~TSDFVolume() = default; @@ -57,20 +58,19 @@ class TSDFVolumeCPU : public TSDFVolume TSDFVolumeCPU(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, Vec3i _resolution, bool zFirstMemOrder = true); - virtual void integrate(InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, - const cv::kinfu::Intr& intrinsics) override; - virtual void raycast(const cv::Matx44f& cameraPose, const cv::kinfu::Intr& intrinsics, - cv::Size frameSize, cv::OutputArray points, - cv::OutputArray normals) const override; + virtual void integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, + const kinfu::Intr& intrinsics, const int frameId = 0) override; + virtual void raycast(const Matx44f& cameraPose, const kinfu::Intr& intrinsics, const Size& frameSize, + OutputArray points, OutputArray normals) const override; - virtual void fetchNormals(cv::InputArray points, cv::OutputArray _normals) const override; - virtual void fetchPointsNormals(cv::OutputArray points, cv::OutputArray normals) const override; + virtual void fetchNormals(InputArray points, OutputArray _normals) const override; + virtual void fetchPointsNormals(OutputArray points, OutputArray normals) const override; virtual void reset() override; - virtual TsdfVoxel at(const cv::Vec3i& volumeIdx) const; + virtual TsdfVoxel at(const Vec3i& volumeIdx) const; - float interpolateVoxel(cv::Point3f p) const; - Point3f getNormalVoxel(cv::Point3f p) const; + float interpolateVoxel(const cv::Point3f& p) const; + Point3f getNormalVoxel(const cv::Point3f& p) const; #if USE_INTRINSICS float interpolateVoxel(const v_float32x4& p) const; @@ -89,17 +89,16 @@ class TSDFVolumeGPU : public TSDFVolume { public: // dimension in voxels, size in meters - TSDFVolumeGPU(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, float _truncDist, + TSDFVolumeGPU(float _voxelSize, Matx44f _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, Point3i _resolution); - virtual void integrate(InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, - const cv::kinfu::Intr& intrinsics) override; - virtual void raycast(const cv::Matx44f& cameraPose, const cv::kinfu::Intr& intrinsics, - cv::Size frameSize, cv::OutputArray _points, - cv::OutputArray _normals) const override; + virtual void integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, + const kinfu::Intr& intrinsics, const int frameId = 0) override; + virtual void raycast(const Matx44f& cameraPose, const kinfu::Intr& intrinsics, const Size& frameSize, + OutputArray _points, OutputArray _normals) const override; - virtual void fetchPointsNormals(cv::OutputArray points, cv::OutputArray normals) const override; - virtual void fetchNormals(cv::InputArray points, cv::OutputArray normals) const override; + virtual void fetchPointsNormals(OutputArray points, OutputArray normals) const override; + virtual void fetchNormals(InputArray points, OutputArray normals) const override; virtual void reset() override; @@ -112,8 +111,9 @@ class TSDFVolumeGPU : public TSDFVolume UMat volume; }; #endif -cv::Ptr makeTSDFVolume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, - float _truncDist, int _maxWeight, Point3i _resolution); +Ptr makeTSDFVolume(float _voxelSize, Matx44f _pose, float _raycastStepFactor, + float _truncDist, int _maxWeight, Point3i _resolution); +Ptr makeTSDFVolume(const VolumeParams& _params); } // namespace kinfu } // namespace cv #endif diff --git a/modules/rgbd/src/utils.hpp b/modules/rgbd/src/utils.hpp index b7febed57a7..0b963675390 100644 --- a/modules/rgbd/src/utils.hpp +++ b/modules/rgbd/src/utils.hpp @@ -9,6 +9,8 @@ #ifndef __OPENCV_RGBD_UTILS_HPP__ #define __OPENCV_RGBD_UTILS_HPP__ +#include "precomp.hpp" + namespace cv { namespace rgbd diff --git a/modules/rgbd/src/volume.cpp b/modules/rgbd/src/volume.cpp index 5d83ad5c1a8..8177213e8ab 100644 --- a/modules/rgbd/src/volume.cpp +++ b/modules/rgbd/src/volume.cpp @@ -2,19 +2,79 @@ // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html -#include "precomp.hpp" #include -#include "tsdf.hpp" #include "hash_tsdf.hpp" +#include "opencv2/core/base.hpp" +#include "precomp.hpp" +#include "tsdf.hpp" namespace cv { namespace kinfu { -cv::Ptr makeVolume(VolumeType _volumeType, float _voxelSize, cv::Matx44f _pose, - float _raycastStepFactor, float _truncDist, int _maxWeight, - float _truncateThreshold, Vec3i _resolution) +Ptr VolumeParams::defaultParams(VolumeType _volumeType) +{ + VolumeParams params; + params.type = _volumeType; + params.maxWeight = 64; + params.raycastStepFactor = 0.25f; + params.unitResolution = 0; // unitResolution not used for TSDF + float volumeSize = 3.0f; + params.pose = Affine3f().translate(Vec3f(-volumeSize / 2.f, -volumeSize / 2.f, 0.5f)); + if(params.type == VolumeType::TSDF) + { + params.resolution = Vec3i::all(512); + params.voxelSize = volumeSize / 512.f; + params.depthTruncThreshold = 0.f; // depthTruncThreshold not required for TSDF + params.tsdfTruncDist = 7 * params.voxelSize; //! About 0.04f in meters + return makePtr(params); + } + else if(params.type == VolumeType::HASHTSDF) + { + params.unitResolution = 16; + params.voxelSize = volumeSize / 512.f; + params.depthTruncThreshold = rgbd::Odometry::DEFAULT_MAX_DEPTH(); + params.tsdfTruncDist = 7 * params.voxelSize; //! About 0.04f in meters + return makePtr(params); + } + CV_Error(Error::StsBadArg, "Invalid VolumeType does not have parameters"); +} + +Ptr VolumeParams::coarseParams(VolumeType _volumeType) +{ + Ptr params = defaultParams(_volumeType); + + params->raycastStepFactor = 0.75f; + float volumeSize = 3.0f; + if(params->type == VolumeType::TSDF) + { + params->resolution = Vec3i::all(128); + params->voxelSize = volumeSize / 128.f; + params->tsdfTruncDist = 2 * params->voxelSize; //! About 0.04f in meters + return params; + } + else if(params->type == VolumeType::HASHTSDF) + { + params->voxelSize = volumeSize / 128.f; + params->tsdfTruncDist = 2 * params->voxelSize; //! About 0.04f in meters + return params; + } + CV_Error(Error::StsBadArg, "Invalid VolumeType does not have parameters"); +} + +Ptr makeVolume(const VolumeParams& _volumeParams) +{ + if(_volumeParams.type == VolumeType::TSDF) + return kinfu::makeTSDFVolume(_volumeParams); + else if(_volumeParams.type == VolumeType::HASHTSDF) + return kinfu::makeHashTSDFVolume(_volumeParams); + CV_Error(Error::StsBadArg, "Invalid VolumeType does not have parameters"); +} + +Ptr makeVolume(VolumeType _volumeType, float _voxelSize, Matx44f _pose, + float _raycastStepFactor, float _truncDist, int _maxWeight, + float _truncateThreshold, Vec3i _resolution) { Point3i _presolution = _resolution; if (_volumeType == VolumeType::TSDF) @@ -24,11 +84,10 @@ cv::Ptr makeVolume(VolumeType _volumeType, float _voxelSize, cv::Matx44f } else if (_volumeType == VolumeType::HASHTSDF) { - return makeHashTSDFVolume(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, - _truncateThreshold); + return makeHashTSDFVolume( + _voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, _truncateThreshold); } - else - return nullptr; + CV_Error(Error::StsBadArg, "Invalid VolumeType does not have parameters"); } } // namespace kinfu From 0b5ded46375c64007149a85e70b2c5e5ac98c41d Mon Sep 17 00:00:00 2001 From: DumDereDum <46279571+DumDereDum@users.noreply.github.com> Date: Mon, 19 Oct 2020 20:20:35 +0300 Subject: [PATCH 05/29] Merge pull request #2698 from DumDereDum:new_HashTSDF_implementation New HashTSDF implementation * create new variables * rewrite reset() * first valid version of new HasHTSDF * some warning fixes * create lambda raycast * reduce time raycast * minor fix * minor fix volDims * changed _atVolumeUnit, reduce memory consumption * delete older inmplemetation of atVolumeUnit * changes _at * AAA, I want to cry! * it works! * it works twice o_o * minor fix * new adding to volumes * delete volDims at strust VolumeUnit * new names of vars * rename one var * minor fix * new resize volumes * rename volUnitsMatrix * minor fix in at function * add tsdf_functions.hpp * minor fix * remove two args at _at function signature * solved the link problem with tsdf_functions * build fix * build fix 1 * build fix 2 * build fix 3 * build fix 4 * replace integrateVolumeUnit to tsdf_functions and fix 2 warnings * docs fix * remove extra args at atVolumeUnit signature * change frame params checking * move volStrides to CPU class * inline convertion functions in tsdf_functions * minor fix * add SIMD version of integrateVolumeUnit * fix something :) * docs fix * warning fix * add degub asserts * replace vars initialization with reset() * remove volDims var * new resize buffer * minor vars name fix * docs fix * warning fix * minor fix * minor fix 1 * remove dbg asserts Co-authored-by: arsaratovtsev --- modules/rgbd/src/hash_tsdf.cpp | 387 +++++++++++++++------------- modules/rgbd/src/hash_tsdf.hpp | 69 ++--- modules/rgbd/src/tsdf.cpp | 138 +--------- modules/rgbd/src/tsdf_functions.cpp | 377 +++++++++++++++++++++++++++ modules/rgbd/src/tsdf_functions.hpp | 48 ++++ 5 files changed, 674 insertions(+), 345 deletions(-) create mode 100644 modules/rgbd/src/tsdf_functions.cpp create mode 100644 modules/rgbd/src/tsdf_functions.hpp diff --git a/modules/rgbd/src/hash_tsdf.cpp b/modules/rgbd/src/hash_tsdf.cpp index 69baad8e360..3c5d2d5d43d 100644 --- a/modules/rgbd/src/hash_tsdf.cpp +++ b/modules/rgbd/src/hash_tsdf.cpp @@ -17,34 +17,22 @@ #include "utils.hpp" #define USE_INTERPOLATION_IN_GETNORMAL 1 - +#define VOLUMES_SIZE 1024 namespace cv { namespace kinfu { -static inline TsdfType floatToTsdf(float num) -{ - //CV_Assert(-1 < num <= 1); - int8_t res = int8_t(num * (-128.f)); - res = res ? res : (num < 0 ? 1 : -1); - return res; -} - -static inline float tsdfToFloat(TsdfType num) -{ - return float(num) * (-1.f / 128.f); -} - -HashTSDFVolume::HashTSDFVolume(float _voxelSize, const Matx44f& _pose, float _raycastStepFactor, float _truncDist, - int _maxWeight, float _truncateThreshold, int _volumeUnitRes, bool _zFirstMemOrder) +HashTSDFVolume::HashTSDFVolume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, + float _truncDist, int _maxWeight, float _truncateThreshold, + int _volumeUnitRes, bool _zFirstMemOrder) : Volume(_voxelSize, _pose, _raycastStepFactor), - maxWeight(_maxWeight), - truncateThreshold(_truncateThreshold), - volumeUnitResolution(_volumeUnitRes), - volumeUnitSize(voxelSize * volumeUnitResolution), - zFirstMemOrder(_zFirstMemOrder) + maxWeight(_maxWeight), + truncateThreshold(_truncateThreshold), + volumeUnitResolution(_volumeUnitRes), + volumeUnitSize(voxelSize* volumeUnitResolution), + zFirstMemOrder(_zFirstMemOrder) { truncDist = std::max(_truncDist, 4.0f * voxelSize); } @@ -54,6 +42,22 @@ HashTSDFVolumeCPU::HashTSDFVolumeCPU(float _voxelSize, const Matx44f& _pose, flo :HashTSDFVolume(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, _truncateThreshold, _volumeUnitRes, _zFirstMemOrder) { + int xdim, ydim, zdim; + if (zFirstMemOrder) + { + xdim = volumeUnitResolution * volumeUnitResolution; + ydim = volumeUnitResolution; + zdim = 1; + } + else + { + xdim = 1; + ydim = volumeUnitResolution; + zdim = volumeUnitResolution * volumeUnitResolution; + } + volStrides = Vec4i(xdim, ydim, zdim); + + reset(); } HashTSDFVolumeCPU::HashTSDFVolumeCPU(const VolumeParams& _params, bool _zFirstMemOrder) @@ -65,7 +69,8 @@ HashTSDFVolumeCPU::HashTSDFVolumeCPU(const VolumeParams& _params, bool _zFirstMe void HashTSDFVolumeCPU::reset() { CV_TRACE_FUNCTION(); - volumeUnits.clear(); + lastVolIndex = 0; + volUnitsData = cv::Mat(VOLUMES_SIZE, volumeUnitResolution * volumeUnitResolution * volumeUnitResolution, rawType()); } void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, const Intr& intrinsics, const int frameId) @@ -84,6 +89,7 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma VolumeUnitIndexSet newIndices; Mutex mutex; Range allocateRange(0, depth.rows); + auto AllocateVolumeUnitsInvoker = [&](const Range& range) { VolumeUnitIndexSet localAccessVolUnits; for (int y = range.start; y < range.end; y += depthStride) @@ -130,13 +136,22 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma parallel_for_(allocateRange, AllocateVolumeUnitsInvoker); //! Perform the allocation - int res = volumeUnitResolution; - Point3i volumeDims(res, res, res); for (auto idx : newIndices) { VolumeUnit& vu = volumeUnits[idx]; Matx44f subvolumePose = pose.translate(volumeUnitIdxToVolume(idx)).matrix; - vu.pVolume = makePtr(voxelSize, subvolumePose, raycastStepFactor, truncDist, maxWeight, volumeDims); + + vu.pose = subvolumePose; + vu.index = lastVolIndex; lastVolIndex++; + if (lastVolIndex > VolumeIndex(volUnitsData.size().height)) + { + volUnitsData.resize((lastVolIndex - 1) * 2); + } + volUnitsData.row(vu.index).forEach([](VecTsdfVoxel& vv, const int* /* position */) + { + TsdfVoxel& v = reinterpret_cast(vv); + v.tsdf = floatToTsdf(0.0f); v.weight = 0; + }); //! This volume unit will definitely be required for current integration vu.lastVisibleIndex = frameId; vu.isActive = true; @@ -158,7 +173,7 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma for (int i = range.start; i < range.end; ++i) { Vec3i tsdf_idx = totalVolUnits[i]; - VolumeUnitMap::iterator it = volumeUnits.find(tsdf_idx); + VolumeUnitIndexes::iterator it = volumeUnits.find(tsdf_idx); if (it == volumeUnits.end()) return; @@ -179,12 +194,21 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma } }); + Vec6f newParams((float)depth.rows, (float)depth.cols, + intrinsics.fx, intrinsics.fy, + intrinsics.cx, intrinsics.cy); + if ( !(frameParams==newParams) ) + { + frameParams = newParams; + pixNorms = preCalculationPixNorm(depth, intrinsics); + } + //! Integrate the correct volumeUnits parallel_for_(Range(0, (int)totalVolUnits.size()), [&](const Range& range) { for (int i = range.start; i < range.end; i++) { Vec3i tsdf_idx = totalVolUnits[i]; - VolumeUnitMap::iterator it = volumeUnits.find(tsdf_idx); + VolumeUnitIndexes::iterator it = volumeUnits.find(tsdf_idx); if (it == volumeUnits.end()) return; @@ -192,7 +216,9 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma if (volumeUnit.isActive) { //! The volume unit should already be added into the Volume from the allocator - volumeUnit.pVolume->integrate(depth, depthFactor, cameraPose, intrinsics); + integrateVolumeUnit(truncDist, voxelSize, maxWeight, volumeUnit.pose, volumeUnitResolution, volStrides, depth, + depthFactor, cameraPose, intrinsics, pixNorms, volUnitsData.row(volumeUnit.index)); + //! Ensure all active volumeUnits are set to inactive for next integration volumeUnit.isActive = false; } @@ -223,47 +249,70 @@ cv::Vec3i HashTSDFVolumeCPU::volumeToVoxelCoord(const cv::Point3f& point) const cvFloor(point.z * voxelSizeInv)); } -TsdfVoxel HashTSDFVolumeCPU::at(const Vec3i& volumeIdx) const +inline TsdfVoxel HashTSDFVolumeCPU::_at(const cv::Vec3i& volumeIdx, VolumeIndex indx) const +{ + //! Out of bounds + if ((volumeIdx[0] >= volumeUnitResolution || volumeIdx[0] < 0) || + (volumeIdx[1] >= volumeUnitResolution || volumeIdx[1] < 0) || + (volumeIdx[2] >= volumeUnitResolution || volumeIdx[2] < 0)) + { + TsdfVoxel dummy; + dummy.tsdf = floatToTsdf(1.0f); + dummy.weight = 0; + return dummy; + } + + const TsdfVoxel* volData = volUnitsData.ptr(indx); + int coordBase = + volumeIdx[0] * volStrides[0] + volumeIdx[1] * volStrides[1] + volumeIdx[2] * volStrides[2]; + return volData[coordBase]; +} + +inline TsdfVoxel HashTSDFVolumeCPU::at(const cv::Vec3i& volumeIdx) const + { Vec3i volumeUnitIdx = Vec3i(cvFloor(volumeIdx[0] / volumeUnitResolution), cvFloor(volumeIdx[1] / volumeUnitResolution), cvFloor(volumeIdx[2] / volumeUnitResolution)); - VolumeUnitMap::const_iterator it = volumeUnits.find(volumeUnitIdx); + VolumeUnitIndexes::const_iterator it = volumeUnits.find(volumeUnitIdx); + if (it == volumeUnits.end()) { TsdfVoxel dummy; - dummy.tsdf = floatToTsdf(1.f); + dummy.tsdf = floatToTsdf(1.f); dummy.weight = 0; return dummy; } - Ptr volumeUnit = std::dynamic_pointer_cast(it->second.pVolume); - Vec3i volUnitLocalIdx = volumeIdx - Vec3i(volumeUnitIdx[0] * volumeUnitResolution, - volumeUnitIdx[1] * volumeUnitResolution, - volumeUnitIdx[2] * volumeUnitResolution); + cv::Vec3i volUnitLocalIdx = volumeIdx - cv::Vec3i(volumeUnitIdx[0] * volumeUnitResolution, + volumeUnitIdx[1] * volumeUnitResolution, + volumeUnitIdx[2] * volumeUnitResolution); + + volUnitLocalIdx = + cv::Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); + return _at(volUnitLocalIdx, it->second.index); - volUnitLocalIdx = Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); - return volumeUnit->at(volUnitLocalIdx); } TsdfVoxel HashTSDFVolumeCPU::at(const Point3f& point) const { - Vec3i volumeUnitIdx = volumeToVolumeUnitIdx(point); - VolumeUnitMap::const_iterator it = volumeUnits.find(volumeUnitIdx); + cv::Vec3i volumeUnitIdx = volumeToVolumeUnitIdx(point); + VolumeUnitIndexes::const_iterator it = volumeUnits.find(volumeUnitIdx); + if (it == volumeUnits.end()) { TsdfVoxel dummy; - dummy.tsdf = floatToTsdf(1.f); + dummy.tsdf = floatToTsdf(1.f); dummy.weight = 0; return dummy; } - Ptr volumeUnit = std::dynamic_pointer_cast(it->second.pVolume); - Point3f volumeUnitPos = volumeUnitIdxToVolume(volumeUnitIdx); - Vec3i volUnitLocalIdx = volumeToVoxelCoord(point - volumeUnitPos); - volUnitLocalIdx = Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); - return volumeUnit->at(volUnitLocalIdx); + cv::Point3f volumeUnitPos = volumeUnitIdxToVolume(volumeUnitIdx); + cv::Vec3i volUnitLocalIdx = volumeToVoxelCoord(point - volumeUnitPos); + volUnitLocalIdx = + cv::Vec3i(abs(volUnitLocalIdx[0]), abs(volUnitLocalIdx[1]), abs(volUnitLocalIdx[2])); + return _at(volUnitLocalIdx, it->second.index); } static inline Vec3i voxelToVolumeUnitIdx(const Vec3i& pt, const int vuRes) @@ -282,27 +331,25 @@ static inline Vec3i voxelToVolumeUnitIdx(const Vec3i& pt, const int vuRes) } } -inline TsdfVoxel atVolumeUnit(const Vec3i& point, const Vec3i& volumeUnitIdx, VolumeUnitMap::const_iterator it, - VolumeUnitMap::const_iterator vend, int unitRes) +TsdfVoxel HashTSDFVolumeCPU::atVolumeUnit(const Vec3i& point, const Vec3i& volumeUnitIdx, VolumeUnitIndexes::const_iterator it) const { - if (it == vend) + if (it == volumeUnits.end()) { TsdfVoxel dummy; dummy.tsdf = floatToTsdf(1.f); dummy.weight = 0; return dummy; } - Ptr volumeUnit = std::dynamic_pointer_cast(it->second.pVolume); - - Vec3i volUnitLocalIdx = point - volumeUnitIdx * unitRes; + Vec3i volUnitLocalIdx = point - volumeUnitIdx * volumeUnitResolution; // expanding at(), removing bounds check - const TsdfVoxel* volData = volumeUnit->volume.ptr(); - Vec4i volDims = volumeUnit->volDims; - int coordBase = volUnitLocalIdx[0] * volDims[0] + volUnitLocalIdx[1] * volDims[1] + volUnitLocalIdx[2] * volDims[2]; + const TsdfVoxel* volData = volUnitsData.ptr(it->second.index); + int coordBase = volUnitLocalIdx[0] * volStrides[0] + volUnitLocalIdx[1] * volStrides[1] + volUnitLocalIdx[2] * volStrides[2]; return volData[coordBase]; } + + #if USE_INTRINSICS inline float interpolate(float tx, float ty, float tz, float vx[8]) { @@ -344,7 +391,7 @@ float HashTSDFVolumeCPU::interpolateVoxelPoint(const Point3f& point) const // A small hash table to reduce a number of find() calls bool queried[8]; - VolumeUnitMap::const_iterator iterMap[8]; + VolumeUnitIndexes::const_iterator iterMap[8]; for (int i = 0; i < 8; i++) { iterMap[i] = volumeUnits.end(); @@ -374,9 +421,8 @@ float HashTSDFVolumeCPU::interpolateVoxelPoint(const Point3f& point) const iterMap[dictIdx] = it; queried[dictIdx] = true; } - //VolumeUnitMap::const_iterator it = volumeUnits.find(volumeUnitIdx); - vx[i] = atVolumeUnit(pt, volumeUnitIdx, it, volumeUnits.end(), volumeUnitResolution).tsdf; + vx[i] = atVolumeUnit(pt, volumeUnitIdx, it).tsdf; } return interpolate(tx, ty, tz, vx); @@ -397,7 +443,7 @@ Point3f HashTSDFVolumeCPU::getNormalVoxel(const Point3f &point) const // A small hash table to reduce a number of find() calls bool queried[8]; - VolumeUnitMap::const_iterator iterMap[8]; + VolumeUnitIndexes::const_iterator iterMap[8]; for (int i = 0; i < 8; i++) { iterMap[i] = volumeUnits.end(); @@ -438,9 +484,8 @@ Point3f HashTSDFVolumeCPU::getNormalVoxel(const Point3f &point) const iterMap[dictIdx] = it; queried[dictIdx] = true; } - //VolumeUnitMap::const_iterator it = volumeUnits.find(volumeUnitIdx); - vals[i] = tsdfToFloat(atVolumeUnit(pt, volumeUnitIdx, it, volumeUnits.end(), volumeUnitResolution).tsdf); + vals[i] = tsdfToFloat(atVolumeUnit(pt, volumeUnitIdx, it).tsdf); } #if !USE_INTERPOLATION_IN_GETNORMAL @@ -527,26 +572,33 @@ Point3f HashTSDFVolumeCPU::getNormalVoxel(const Point3f &point) const return nv < 0.0001f ? nan3 : normal / nv; } -struct HashRaycastInvoker : ParallelLoopBody +void HashTSDFVolumeCPU::raycast(const Matx44f& cameraPose, const kinfu::Intr& intrinsics, const Size& frameSize, + OutputArray _points, OutputArray _normals) const { - HashRaycastInvoker(Points& _points, Normals& _normals, const Matx44f& cameraPose, const Intr& intrinsics, - const HashTSDFVolumeCPU& _volume) - : ParallelLoopBody(), - points(_points), - normals(_normals), - volume(_volume), - tstep(_volume.truncDist * _volume.raycastStepFactor), - cam2vol(volume.pose.inv() * Affine3f(cameraPose)), - vol2cam(Affine3f(cameraPose.inv()) * volume.pose), - reproj(intrinsics.makeReprojector()) - { - } + CV_TRACE_FUNCTION(); + CV_Assert(frameSize.area() > 0); + + _points.create(frameSize, POINT_TYPE); + _normals.create(frameSize, POINT_TYPE); + + Points points1 = _points.getMat(); + Normals normals1 = _normals.getMat(); + + Points& points(points1); + Normals& normals(normals1); + const HashTSDFVolumeCPU& volume(*this); + const float tstep(volume.truncDist * volume.raycastStepFactor); + const Affine3f cam2vol(volume.pose.inv() * Affine3f(cameraPose)); + const Affine3f vol2cam(Affine3f(cameraPose.inv()) * volume.pose); + const Intr::Reprojector reproj(intrinsics.makeReprojector()); + + const int nstripes = -1; - virtual void operator()(const Range& range) const override + auto _HashRaycastInvoker = [&](const Range& range) { const Point3f cam2volTrans = cam2vol.translation(); - const Matx33f cam2volRot = cam2vol.rotation(); - const Matx33f vol2camRot = vol2cam.rotation(); + const Matx33f cam2volRot = cam2vol.rotation(); + const Matx33f vol2camRot = vol2cam.rotation(); const float blockSize = volume.volumeUnitSize; @@ -564,41 +616,45 @@ struct HashRaycastInvoker : ParallelLoopBody Point3f orig = cam2volTrans; Point3f rayDirV = normalize(Vec3f(cam2volRot * reproj(Point3f(float(x), float(y), 1.f)))); - float tmin = 0; - float tmax = volume.truncateThreshold; + float tmin = 0; + float tmax = volume.truncateThreshold; float tcurr = tmin; - Vec3i prevVolumeUnitIdx = - Vec3i(std::numeric_limits::min(), std::numeric_limits::min(), - std::numeric_limits::min()); + cv::Vec3i prevVolumeUnitIdx = + cv::Vec3i(std::numeric_limits::min(), std::numeric_limits::min(), + std::numeric_limits::min()); - float tprev = tcurr; + + float tprev = tcurr; float prevTsdf = volume.truncDist; Ptr currVolumeUnit; while (tcurr < tmax) { - Point3f currRayPos = orig + tcurr * rayDirV; - Vec3i currVolumeUnitIdx = volume.volumeToVolumeUnitIdx(currRayPos); + Point3f currRayPos = orig + tcurr * rayDirV; + cv::Vec3i currVolumeUnitIdx = volume.volumeToVolumeUnitIdx(currRayPos); + - VolumeUnitMap::const_iterator it = volume.volumeUnits.find(currVolumeUnitIdx); + VolumeUnitIndexes::const_iterator it = volume.volumeUnits.find(currVolumeUnitIdx); float currTsdf = prevTsdf; - int currWeight = 0; - float stepSize = 0.5f * blockSize; - Vec3i volUnitLocalIdx; + int currWeight = 0; + float stepSize = 0.5f * blockSize; + cv::Vec3i volUnitLocalIdx; + //! The subvolume exists in hashtable if (it != volume.volumeUnits.end()) { - currVolumeUnit = std::dynamic_pointer_cast(it->second.pVolume); - Point3f currVolUnitPos = volume.volumeUnitIdxToVolume(currVolumeUnitIdx); - volUnitLocalIdx = volume.volumeToVoxelCoord(currRayPos - currVolUnitPos); + cv::Point3f currVolUnitPos = + volume.volumeUnitIdxToVolume(currVolumeUnitIdx); + volUnitLocalIdx = volume.volumeToVoxelCoord(currRayPos - currVolUnitPos); + //! TODO: Figure out voxel interpolation - TsdfVoxel currVoxel = currVolumeUnit->at(volUnitLocalIdx); - currTsdf = tsdfToFloat(currVoxel.tsdf); - currWeight = currVoxel.weight; - stepSize = tstep; + TsdfVoxel currVoxel = _at(volUnitLocalIdx, it->second.index); + currTsdf = tsdfToFloat(currVoxel.tsdf); + currWeight = currVoxel.weight; + stepSize = tstep; } //! Surface crossing if (prevTsdf > 0.f && currTsdf <= 0.f && currWeight > 0) @@ -612,129 +668,92 @@ struct HashRaycastInvoker : ParallelLoopBody if (!isNaN(nv)) { normal = vol2camRot * nv; - point = vol2cam * pv; + point = vol2cam * pv; } } break; } prevVolumeUnitIdx = currVolumeUnitIdx; - prevTsdf = currTsdf; - tprev = tcurr; + prevTsdf = currTsdf; + tprev = tcurr; tcurr += stepSize; } ptsRow[x] = toPtype(point); nrmRow[x] = toPtype(normal); } } - } + }; - Points& points; - Normals& normals; - const HashTSDFVolumeCPU& volume; - const float tstep; - const Affine3f cam2vol; - const Affine3f vol2cam; - const Intr::Reprojector reproj; -}; + parallel_for_(Range(0, points.rows), _HashRaycastInvoker, nstripes); +} -void HashTSDFVolumeCPU::raycast(const Matx44f& cameraPose, const kinfu::Intr& intrinsics, const Size& frameSize, - OutputArray _points, OutputArray _normals) const +void HashTSDFVolumeCPU::fetchPointsNormals(OutputArray _points, OutputArray _normals) const { CV_TRACE_FUNCTION(); - CV_Assert(frameSize.area() > 0); - _points.create(frameSize, POINT_TYPE); - _normals.create(frameSize, POINT_TYPE); - - Points points = _points.getMat(); - Normals normals = _normals.getMat(); + if (_points.needed()) + { + std::vector> pVecs, nVecs; - HashRaycastInvoker ri(points, normals, cameraPose, intrinsics, *this); + std::vector totalVolUnits; + for (const auto& keyvalue : volumeUnits) + { + totalVolUnits.push_back(keyvalue.first); + } + Range fetchRange(0, (int)totalVolUnits.size()); + const int nstripes = -1; - const int nstripes = -1; - parallel_for_(Range(0, points.rows), ri, nstripes); -} + const HashTSDFVolumeCPU& volume(*this); + bool needNormals(_normals.needed()); + Mutex mutex; -struct HashFetchPointsNormalsInvoker : ParallelLoopBody -{ - HashFetchPointsNormalsInvoker(const HashTSDFVolumeCPU& _volume, const std::vector& _totalVolUnits, - std::vector>& _pVecs, std::vector>& _nVecs, - bool _needNormals) - : ParallelLoopBody(), - volume(_volume), - totalVolUnits(_totalVolUnits), - pVecs(_pVecs), - nVecs(_nVecs), - needNormals(_needNormals) - { - } - virtual void operator()(const Range& range) const override - { - std::vector points, normals; - for (int i = range.start; i < range.end; i++) + auto HashFetchPointsNormalsInvoker = [&](const Range& range) { - Vec3i tsdf_idx = totalVolUnits[i]; - VolumeUnitMap::const_iterator it = volume.volumeUnits.find(tsdf_idx); - Point3f base_point = volume.volumeUnitIdxToVolume(tsdf_idx); - if (it != volume.volumeUnits.end()) + + std::vector points, normals; + for (int i = range.start; i < range.end; i++) { - Ptr volumeUnit = std::dynamic_pointer_cast(it->second.pVolume); - std::vector localPoints; - std::vector localNormals; - for (int x = 0; x < volume.volumeUnitResolution; x++) - for (int y = 0; y < volume.volumeUnitResolution; y++) - for (int z = 0; z < volume.volumeUnitResolution; z++) - { - Vec3i voxelIdx(x, y, z); - TsdfVoxel voxel = volumeUnit->at(voxelIdx); + cv::Vec3i tsdf_idx = totalVolUnits[i]; + - if (voxel.tsdf != -128 && voxel.weight != 0) + VolumeUnitIndexes::const_iterator it = volume.volumeUnits.find(tsdf_idx); + Point3f base_point = volume.volumeUnitIdxToVolume(tsdf_idx); + if (it != volume.volumeUnits.end()) + { + std::vector localPoints; + std::vector localNormals; + for (int x = 0; x < volume.volumeUnitResolution; x++) + for (int y = 0; y < volume.volumeUnitResolution; y++) + for (int z = 0; z < volume.volumeUnitResolution; z++) { - Point3f point = base_point + volume.voxelCoordToVolume(voxelIdx); - localPoints.push_back(toPtype(point)); - if (needNormals) + cv::Vec3i voxelIdx(x, y, z); + TsdfVoxel voxel = _at(voxelIdx, it->second.index); + + if (voxel.tsdf != -128 && voxel.weight != 0) { - Point3f normal = volume.getNormalVoxel(point); - localNormals.push_back(toPtype(normal)); + Point3f point = base_point + volume.voxelCoordToVolume(voxelIdx); + localPoints.push_back(toPtype(point)); + if (needNormals) + { + Point3f normal = volume.getNormalVoxel(point); + localNormals.push_back(toPtype(normal)); + } } } - } - AutoLock al(mutex); - pVecs.push_back(localPoints); - nVecs.push_back(localNormals); + AutoLock al(mutex); + pVecs.push_back(localPoints); + nVecs.push_back(localNormals); + } } - } - } + }; - const HashTSDFVolumeCPU& volume; - std::vector totalVolUnits; - std::vector>& pVecs; - std::vector>& nVecs; - const TsdfVoxel* volDataStart; - bool needNormals; - mutable Mutex mutex; -}; + parallel_for_(fetchRange, HashFetchPointsNormalsInvoker, nstripes); -void HashTSDFVolumeCPU::fetchPointsNormals(OutputArray _points, OutputArray _normals) const -{ - CV_TRACE_FUNCTION(); - if (_points.needed()) - { - std::vector> pVecs, nVecs; - std::vector totalVolUnits; - for (const auto& keyvalue : volumeUnits) - { - totalVolUnits.push_back(keyvalue.first); - } - HashFetchPointsNormalsInvoker fi(*this, totalVolUnits, pVecs, nVecs, _normals.needed()); - Range range(0, (int)totalVolUnits.size()); - const int nstripes = -1; - parallel_for_(range, fi, nstripes); std::vector points, normals; for (size_t i = 0; i < pVecs.size(); i++) { diff --git a/modules/rgbd/src/hash_tsdf.hpp b/modules/rgbd/src/hash_tsdf.hpp index cbe4cffea03..31bf026785b 100644 --- a/modules/rgbd/src/hash_tsdf.hpp +++ b/modules/rgbd/src/hash_tsdf.hpp @@ -9,20 +9,30 @@ #include #include -#include "tsdf.hpp" +#include "tsdf_functions.hpp" namespace cv { namespace kinfu { -struct VolumeUnit +class HashTSDFVolume : public Volume { - VolumeUnit() : pVolume(nullptr){}; - ~VolumeUnit() = default; + public: + // dimension in voxels, size in meters + //! Use fixed volume cuboid + HashTSDFVolume(float _voxelSize, cv::Matx44f _pose, float _raycastStepFactor, float _truncDist, + int _maxWeight, float _truncateThreshold, int _volumeUnitRes, + bool zFirstMemOrder = true); - Ptr pVolume; - int lastVisibleIndex = 0; - bool isActive; + virtual ~HashTSDFVolume() = default; + + public: + int maxWeight; + float truncDist; + float truncateThreshold; + int volumeUnitResolution; + float volumeUnitSize; + bool zFirstMemOrder; }; //! Spatial hashing @@ -40,32 +50,21 @@ struct tsdf_hash } }; -typedef std::unordered_set VolumeUnitIndexSet; -typedef std::unordered_map VolumeUnitMap; - -class HashTSDFVolume : public Volume +typedef unsigned int VolumeIndex; +struct VolumeUnit { - public: - // dimension in voxels, size in meters - //! Use fixed volume cuboid - HashTSDFVolume(float _voxelSize, const Matx44f& _pose, float _raycastStepFactor, float _truncDist, - int _maxWeight, float _truncateThreshold, int _volumeUnitRes, - bool zFirstMemOrder = true); - - virtual ~HashTSDFVolume() = default; - - public: - int maxWeight; - float truncDist; - float truncateThreshold; - int volumeUnitResolution; - float volumeUnitSize; - bool zFirstMemOrder; + cv::Vec3i coord; + VolumeIndex index; + cv::Matx44f pose; + int lastVisibleIndex = 0; + bool isActive; }; +typedef std::unordered_set VolumeUnitIndexSet; +typedef std::unordered_map VolumeUnitIndexes; + class HashTSDFVolumeCPU : public HashTSDFVolume { - public: // dimension in voxels, size in meters HashTSDFVolumeCPU(float _voxelSize, const Matx44f& _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, @@ -90,7 +89,11 @@ class HashTSDFVolumeCPU : public HashTSDFVolume //! Return the voxel given the point in volume coordinate system i.e., (metric scale 1 unit = //! 1m) - TsdfVoxel at(const Point3f& point) const; + virtual TsdfVoxel at(const cv::Point3f& point) const; + virtual TsdfVoxel _at(const cv::Vec3i& volumeIdx, VolumeIndex indx) const; + + TsdfVoxel atVolumeUnit(const Vec3i& point, const Vec3i& volumeUnitIdx, VolumeUnitIndexes::const_iterator it) const; + float interpolateVoxelPoint(const Point3f& point) const; float interpolateVoxel(const cv::Point3f& point) const; @@ -104,8 +107,12 @@ class HashTSDFVolumeCPU : public HashTSDFVolume Vec3i volumeToVoxelCoord(const Point3f& point) const; public: - //! Hashtable of individual smaller volume units - VolumeUnitMap volumeUnits; + Vec4i volStrides; + Vec6f frameParams; + Mat pixNorms; + VolumeUnitIndexes volumeUnits; + cv::Mat volUnitsData; + VolumeIndex lastVolIndex; }; template diff --git a/modules/rgbd/src/tsdf.cpp b/modules/rgbd/src/tsdf.cpp index ff2a42f87fd..42dbd29a82a 100644 --- a/modules/rgbd/src/tsdf.cpp +++ b/modules/rgbd/src/tsdf.cpp @@ -5,32 +5,14 @@ // This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this module's directory #include "precomp.hpp" -#include "tsdf.hpp" +//#include "tsdf.hpp" +#include "tsdf_functions.hpp" #include "opencl_kernels_rgbd.hpp" namespace cv { namespace kinfu { -static inline v_float32x4 tsdfToFloat_INTR(const v_int32x4& num) -{ - v_float32x4 num128 = v_setall_f32(-1.f / 128.f); - return v_cvt_f32(num) * num128; -} - -static inline TsdfType floatToTsdf(float num) -{ - //CV_Assert(-1 < num <= 1); - int8_t res = int8_t(num * (-128.f)); - res = res ? res : (num < 0 ? 1 : -1); - return res; -} - -static inline float tsdfToFloat(TsdfType num) -{ - return float(num) * (-1.f / 128.f); -} - TSDFVolume::TSDFVolume(float _voxelSize, Matx44f _pose, float _raycastStepFactor, float _truncDist, int _maxWeight, Point3i _resolution, bool zFirstMemOrder) : Volume(_voxelSize, _pose, _raycastStepFactor), @@ -117,85 +99,6 @@ TsdfVoxel TSDFVolumeCPU::at(const Vec3i& volumeIdx) const return volData[coordBase]; } -// SIMD version of that code is manually inlined -#if !USE_INTRINSICS -static const bool fixMissingData = false; - -static inline depthType bilinearDepth(const Depth& m, Point2f pt) -{ - const depthType defaultValue = qnan; - if(pt.x < 0 || pt.x >= m.cols-1 || - pt.y < 0 || pt.y >= m.rows-1) - return defaultValue; - - int xi = cvFloor(pt.x), yi = cvFloor(pt.y); - - const depthType* row0 = m[yi+0]; - const depthType* row1 = m[yi+1]; - - depthType v00 = row0[xi+0]; - depthType v01 = row0[xi+1]; - depthType v10 = row1[xi+0]; - depthType v11 = row1[xi+1]; - - // assume correct depth is positive - bool b00 = v00 > 0; - bool b01 = v01 > 0; - bool b10 = v10 > 0; - bool b11 = v11 > 0; - - if(!fixMissingData) - { - if(!(b00 && b01 && b10 && b11)) - return defaultValue; - else - { - float tx = pt.x - xi, ty = pt.y - yi; - depthType v0 = v00 + tx*(v01 - v00); - depthType v1 = v10 + tx*(v11 - v10); - return v0 + ty*(v1 - v0); - } - } - else - { - int nz = b00 + b01 + b10 + b11; - if(nz == 0) - { - return defaultValue; - } - if(nz == 1) - { - if(b00) return v00; - if(b01) return v01; - if(b10) return v10; - if(b11) return v11; - } - if(nz == 2) - { - if(b00 && b10) v01 = v00, v11 = v10; - if(b01 && b11) v00 = v01, v10 = v11; - if(b00 && b01) v10 = v00, v11 = v01; - if(b10 && b11) v00 = v10, v01 = v11; - if(b00 && b11) v01 = v10 = (v00 + v11)*0.5f; - if(b01 && b10) v00 = v11 = (v01 + v10)*0.5f; - } - if(nz == 3) - { - if(!b00) v00 = v10 + v01 - v11; - if(!b01) v01 = v00 + v11 - v10; - if(!b10) v10 = v00 + v11 - v01; - if(!b11) v11 = v01 + v10 - v00; - } - - float tx = pt.x - xi, ty = pt.y - yi; - depthType v0 = v00 + tx*(v01 - v00); - depthType v1 = v10 + tx*(v11 - v10); - return v0 + ty*(v1 - v0); - } -} -#endif - - struct IntegrateInvoker : ParallelLoopBody { @@ -462,30 +365,6 @@ struct IntegrateInvoker : ParallelLoopBody Mat pixNorms; }; -static cv::Mat preCalculationPixNorm(Depth depth, const Intr& intrinsics) -{ - int height = depth.rows; - int widht = depth.cols; - Point2f fl(intrinsics.fx, intrinsics.fy); - Point2f pp(intrinsics.cx, intrinsics.cy); - Mat pixNorm (height, widht, CV_32F); - std::vector x(widht); - std::vector y(height); - for (int i = 0; i < widht; i++) - x[i] = (i - pp.x) / fl.x; - for (int i = 0; i < height; i++) - y[i] = (i - pp.y) / fl.y; - - for (int i = 0; i < height; i++) - { - for (int j = 0; j < widht; j++) - { - pixNorm.at(i, j) = sqrtf(x[j] * x[j] + y[i] * y[i] + 1.0f); - } - } - return pixNorm; -} - // use depth instead of distance (optimization) void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, const Intr& intrinsics, const int frameId) @@ -495,14 +374,13 @@ void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44 CV_Assert(_depth.type() == DEPTH_TYPE); CV_Assert(!_depth.empty()); Depth depth = _depth.getMat(); - if (!(frameParams[0] == depth.rows && frameParams[1] == depth.cols && - frameParams[2] == intrinsics.fx && frameParams[3] == intrinsics.fy && - frameParams[4] == intrinsics.cx && frameParams[5] == intrinsics.cy)) - { - frameParams[0] = (float)depth.rows; frameParams[1] = (float)depth.cols; - frameParams[2] = intrinsics.fx; frameParams[3] = intrinsics.fy; - frameParams[4] = intrinsics.cx; frameParams[5] = intrinsics.cy; + Vec6f newParams((float)depth.rows, (float)depth.cols, + intrinsics.fx, intrinsics.fy, + intrinsics.cx, intrinsics.cy); + if (!(frameParams == newParams)) + { + frameParams = newParams; pixNorms = preCalculationPixNorm(depth, intrinsics); } diff --git a/modules/rgbd/src/tsdf_functions.cpp b/modules/rgbd/src/tsdf_functions.cpp new file mode 100644 index 00000000000..3eb27742c1e --- /dev/null +++ b/modules/rgbd/src/tsdf_functions.cpp @@ -0,0 +1,377 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +// This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this module's directory + +#include "precomp.hpp" +#include "tsdf_functions.hpp" + +namespace cv { + +namespace kinfu { + +cv::Mat preCalculationPixNorm(Depth depth, const Intr& intrinsics) +{ + int height = depth.rows; + int widht = depth.cols; + Point2f fl(intrinsics.fx, intrinsics.fy); + Point2f pp(intrinsics.cx, intrinsics.cy); + Mat pixNorm(height, widht, CV_32F); + std::vector x(widht); + std::vector y(height); + for (int i = 0; i < widht; i++) + x[i] = (i - pp.x) / fl.x; + for (int i = 0; i < height; i++) + y[i] = (i - pp.y) / fl.y; + + for (int i = 0; i < height; i++) + { + for (int j = 0; j < widht; j++) + { + pixNorm.at(i, j) = sqrtf(x[j] * x[j] + y[i] * y[i] + 1.0f); + } + } + return pixNorm; +} + +const bool fixMissingData = false; +depthType bilinearDepth(const Depth& m, cv::Point2f pt) +{ + const depthType defaultValue = qnan; + if (pt.x < 0 || pt.x >= m.cols - 1 || + pt.y < 0 || pt.y >= m.rows - 1) + return defaultValue; + + int xi = cvFloor(pt.x), yi = cvFloor(pt.y); + + const depthType* row0 = m[yi + 0]; + const depthType* row1 = m[yi + 1]; + + depthType v00 = row0[xi + 0]; + depthType v01 = row0[xi + 1]; + depthType v10 = row1[xi + 0]; + depthType v11 = row1[xi + 1]; + + // assume correct depth is positive + bool b00 = v00 > 0; + bool b01 = v01 > 0; + bool b10 = v10 > 0; + bool b11 = v11 > 0; + + if (!fixMissingData) + { + if (!(b00 && b01 && b10 && b11)) + return defaultValue; + else + { + float tx = pt.x - xi, ty = pt.y - yi; + depthType v0 = v00 + tx * (v01 - v00); + depthType v1 = v10 + tx * (v11 - v10); + return v0 + ty * (v1 - v0); + } + } + else + { + int nz = b00 + b01 + b10 + b11; + if (nz == 0) + { + return defaultValue; + } + if (nz == 1) + { + if (b00) return v00; + if (b01) return v01; + if (b10) return v10; + if (b11) return v11; + } + if (nz == 2) + { + if (b00 && b10) v01 = v00, v11 = v10; + if (b01 && b11) v00 = v01, v10 = v11; + if (b00 && b01) v10 = v00, v11 = v01; + if (b10 && b11) v00 = v10, v01 = v11; + if (b00 && b11) v01 = v10 = (v00 + v11) * 0.5f; + if (b01 && b10) v00 = v11 = (v01 + v10) * 0.5f; + } + if (nz == 3) + { + if (!b00) v00 = v10 + v01 - v11; + if (!b01) v01 = v00 + v11 - v10; + if (!b10) v10 = v00 + v11 - v01; + if (!b11) v11 = v01 + v10 - v00; + } + + float tx = pt.x - xi, ty = pt.y - yi; + depthType v0 = v00 + tx * (v01 - v00); + depthType v1 = v10 + tx * (v11 - v10); + return v0 + ty * (v1 - v0); + } +} + +void integrateVolumeUnit( + float truncDist, float voxelSize, int maxWeight, + cv::Matx44f _pose, int volResolution, Vec4i volStrides, + InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, + const cv::kinfu::Intr& intrinsics, InputArray _pixNorms, InputArray _volume) +{ + CV_TRACE_FUNCTION(); + + CV_Assert(_depth.type() == DEPTH_TYPE); + CV_Assert(!_depth.empty()); + cv::Affine3f vpose(_pose); + Depth depth = _depth.getMat(); + + Range integrateRange(0, volResolution); + + Mat volume = _volume.getMat(); + Mat pixNorms = _pixNorms.getMat(); + const Intr::Projector proj(intrinsics.makeProjector()); + const cv::Affine3f vol2cam(Affine3f(cameraPose.inv()) * vpose); + const float truncDistInv(1.f / truncDist); + const float dfac(1.f / depthFactor); + TsdfVoxel* volDataStart = volume.ptr();; + +#if USE_INTRINSICS + auto IntegrateInvoker = [&](const Range& range) + { + // zStep == vol2cam*(Point3f(x, y, 1)*voxelSize) - basePt; + Point3f zStepPt = Point3f(vol2cam.matrix(0, 2), + vol2cam.matrix(1, 2), + vol2cam.matrix(2, 2)) * voxelSize; + + v_float32x4 zStep(zStepPt.x, zStepPt.y, zStepPt.z, 0); + v_float32x4 vfxy(proj.fx, proj.fy, 0.f, 0.f), vcxy(proj.cx, proj.cy, 0.f, 0.f); + const v_float32x4 upLimits = v_cvt_f32(v_int32x4(depth.cols - 1, depth.rows - 1, 0, 0)); + + for (int x = range.start; x < range.end; x++) + { + TsdfVoxel* volDataX = volDataStart + x * volStrides[0]; + for (int y = 0; y < volResolution; y++) + { + TsdfVoxel* volDataY = volDataX + y * volStrides[1]; + // optimization of camSpace transformation (vector addition instead of matmul at each z) + Point3f basePt = vol2cam * (Point3f((float)x, (float)y, 0) * voxelSize); + v_float32x4 camSpacePt(basePt.x, basePt.y, basePt.z, 0); + + int startZ, endZ; + if (abs(zStepPt.z) > 1e-5) + { + int baseZ = (int)(-basePt.z / zStepPt.z); + if (zStepPt.z > 0) + { + startZ = baseZ; + endZ = volResolution; + } + else + { + startZ = 0; + endZ = baseZ; + } + } + else + { + if (basePt.z > 0) + { + startZ = 0; + endZ = volResolution; + } + else + { + // z loop shouldn't be performed + startZ = endZ = 0; + } + } + startZ = max(0, startZ); + endZ = min(int(volResolution), endZ); + for (int z = startZ; z < endZ; z++) + { + // optimization of the following: + //Point3f volPt = Point3f(x, y, z)*voxelSize; + //Point3f camSpacePt = vol2cam * volPt; + camSpacePt += zStep; + + float zCamSpace = v_reinterpret_as_f32(v_rotate_right<2>(v_reinterpret_as_u32(camSpacePt))).get0(); + if (zCamSpace <= 0.f) + continue; + + v_float32x4 camPixVec = camSpacePt / v_setall_f32(zCamSpace); + v_float32x4 projected = v_muladd(camPixVec, vfxy, vcxy); + // leave only first 2 lanes + projected = v_reinterpret_as_f32(v_reinterpret_as_u32(projected) & + v_uint32x4(0xFFFFFFFF, 0xFFFFFFFF, 0, 0)); + + depthType v; + // bilinearly interpolate depth at projected + { + const v_float32x4& pt = projected; + // check coords >= 0 and < imgSize + v_uint32x4 limits = v_reinterpret_as_u32(pt < v_setzero_f32()) | + v_reinterpret_as_u32(pt >= upLimits); + limits = limits | v_rotate_right<1>(limits); + if (limits.get0()) + continue; + + // xi, yi = floor(pt) + v_int32x4 ip = v_floor(pt); + v_int32x4 ipshift = ip; + int xi = ipshift.get0(); + ipshift = v_rotate_right<1>(ipshift); + int yi = ipshift.get0(); + + const depthType* row0 = depth[yi + 0]; + const depthType* row1 = depth[yi + 1]; + + // v001 = [v(xi + 0, yi + 0), v(xi + 1, yi + 0)] + v_float32x4 v001 = v_load_low(row0 + xi); + // v101 = [v(xi + 0, yi + 1), v(xi + 1, yi + 1)] + v_float32x4 v101 = v_load_low(row1 + xi); + + v_float32x4 vall = v_combine_low(v001, v101); + + // assume correct depth is positive + // don't fix missing data + if (v_check_all(vall > v_setzero_f32())) + { + v_float32x4 t = pt - v_cvt_f32(ip); + float tx = t.get0(); + t = v_reinterpret_as_f32(v_rotate_right<1>(v_reinterpret_as_u32(t))); + v_float32x4 ty = v_setall_f32(t.get0()); + // vx is y-interpolated between rows 0 and 1 + v_float32x4 vx = v001 + ty * (v101 - v001); + float v0 = vx.get0(); + vx = v_reinterpret_as_f32(v_rotate_right<1>(v_reinterpret_as_u32(vx))); + float v1 = vx.get0(); + v = v0 + tx * (v1 - v0); + } + else + continue; + } + + // norm(camPixVec) produces double which is too slow + int _u = (int)projected.get0(); + int _v = (int)v_rotate_right<1>(projected).get0(); + if (!(_u >= 0 && _u < depth.cols && _v >= 0 && _v < depth.rows)) + continue; + float pixNorm = pixNorms.at(_v, _u); + // float pixNorm = sqrt(v_reduce_sum(camPixVec*camPixVec)); + // difference between distances of point and of surface to camera + float sdf = pixNorm * (v * dfac - zCamSpace); + // possible alternative is: + // kftype sdf = norm(camSpacePt)*(v*dfac/camSpacePt.z - 1); + if (sdf >= -truncDist) + { + TsdfType tsdf = floatToTsdf(fmin(1.f, sdf * truncDistInv)); + + TsdfVoxel& voxel = volDataY[z * volStrides[2]]; + WeightType& weight = voxel.weight; + TsdfType& value = voxel.tsdf; + + // update TSDF + value = floatToTsdf((tsdfToFloat(value) * weight + tsdfToFloat(tsdf)) / (weight + 1)); + weight = (weight + 1) < maxWeight ? (weight + 1) : (WeightType) maxWeight; + } + } + } + } + }; +#else + auto IntegrateInvoker = [&](const Range& range) + { + for (int x = range.start; x < range.end; x++) + { + TsdfVoxel* volDataX = volDataStart + x * volStrides[0]; + for (int y = 0; y < volResolution; y++) + { + TsdfVoxel* volDataY = volDataX + y * volStrides[1]; + // optimization of camSpace transformation (vector addition instead of matmul at each z) + Point3f basePt = vol2cam * (Point3f(float(x), float(y), 0.0f) * voxelSize); + Point3f camSpacePt = basePt; + // zStep == vol2cam*(Point3f(x, y, 1)*voxelSize) - basePt; + // zStep == vol2cam*[Point3f(x, y, 1) - Point3f(x, y, 0)]*voxelSize + Point3f zStep = Point3f(vol2cam.matrix(0, 2), + vol2cam.matrix(1, 2), + vol2cam.matrix(2, 2)) * voxelSize; + int startZ, endZ; + if (abs(zStep.z) > 1e-5) + { + int baseZ = int(-basePt.z / zStep.z); + if (zStep.z > 0) + { + startZ = baseZ; + endZ = volResolution; + } + else + { + startZ = 0; + endZ = baseZ; + } + } + else + { + if (basePt.z > 0) + { + startZ = 0; + endZ = volResolution; + } + else + { + // z loop shouldn't be performed + startZ = endZ = 0; + } + } + startZ = max(0, startZ); + endZ = min(int(volResolution), endZ); + + for (int z = startZ; z < endZ; z++) + { + // optimization of the following: + //Point3f volPt = Point3f(x, y, z)*volume.voxelSize; + //Point3f camSpacePt = vol2cam * volPt; + + camSpacePt += zStep; + if (camSpacePt.z <= 0) + continue; + + Point3f camPixVec; + Point2f projected = proj(camSpacePt, camPixVec); + + depthType v = bilinearDepth(depth, projected); + if (v == 0) { + continue; + } + + int _u = projected.x; + int _v = projected.y; + if (!(_u >= 0 && _u < depth.cols && _v >= 0 && _v < depth.rows)) + continue; + float pixNorm = pixNorms.at(_v, _u); + + // difference between distances of point and of surface to camera + float sdf = pixNorm * (v * dfac - camSpacePt.z); + // possible alternative is: + // kftype sdf = norm(camSpacePt)*(v*dfac/camSpacePt.z - 1); + if (sdf >= -truncDist) + { + TsdfType tsdf = floatToTsdf(fmin(1.f, sdf * truncDistInv)); + + TsdfVoxel& voxel = volDataY[z * volStrides[2]]; + WeightType& weight = voxel.weight; + TsdfType& value = voxel.tsdf; + + // update TSDF + value = floatToTsdf((tsdfToFloat(value) * weight + tsdfToFloat(tsdf)) / (weight + 1)); + weight = min(int(weight + 1), int(maxWeight)); + } + } + } + } + }; +#endif + + parallel_for_(integrateRange, IntegrateInvoker); + +} + +} // namespace kinfu +} // namespace cv diff --git a/modules/rgbd/src/tsdf_functions.hpp b/modules/rgbd/src/tsdf_functions.hpp new file mode 100644 index 00000000000..28f776d752f --- /dev/null +++ b/modules/rgbd/src/tsdf_functions.hpp @@ -0,0 +1,48 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html + +// This code is also subject to the license terms in the LICENSE_KinectFusion.md file found in this module's directory + +#ifndef __OPENCV_TSDF_FUNCTIONS_H__ +#define __OPENCV_TSDF_FUNCTIONS_H__ + +#include +#include "tsdf.hpp" + +namespace cv +{ +namespace kinfu +{ + +inline v_float32x4 tsdfToFloat_INTR(const v_int32x4& num) +{ + v_float32x4 num128 = v_setall_f32(-1.f / 128.f); + return v_cvt_f32(num) * num128; +} + +inline TsdfType floatToTsdf(float num) +{ + //CV_Assert(-1 < num <= 1); + int8_t res = int8_t(num * (-128.f)); + res = res ? res : (num < 0 ? 1 : -1); + return res; +} + +inline float tsdfToFloat(TsdfType num) +{ + return float(num) * (-1.f / 128.f); +} + +cv::Mat preCalculationPixNorm(Depth depth, const Intr& intrinsics); +depthType bilinearDepth(const Depth& m, cv::Point2f pt); + +void integrateVolumeUnit( + float truncDist, float voxelSize, int maxWeight, + cv::Matx44f _pose, int volResolution, Vec4i volStrides, + InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, + const cv::kinfu::Intr& intrinsics, InputArray _pixNorms, InputArray _volume); + +} // namespace kinfu +} // namespace cv +#endif From 9b328cf6b95c14ee10cd1c5a30175d82d8d2835e Mon Sep 17 00:00:00 2001 From: DumDereDum <46279571+DumDereDum@users.noreply.github.com> Date: Thu, 22 Oct 2020 00:12:07 +0300 Subject: [PATCH 06/29] Merge pull request #2722 from DumDereDum:tsdf_integrate_replacement Tsdf integrate replacement * replase invoker with integrateVolumeUnit * remove extra code * bug fix * minor fix * it works! Co-authored-by: arsaratovtsev --- modules/rgbd/src/tsdf.cpp | 286 +++----------------------------------- modules/rgbd/src/tsdf.hpp | 1 + 2 files changed, 18 insertions(+), 269 deletions(-) diff --git a/modules/rgbd/src/tsdf.cpp b/modules/rgbd/src/tsdf.cpp index 42dbd29a82a..6e05bebb1d1 100644 --- a/modules/rgbd/src/tsdf.cpp +++ b/modules/rgbd/src/tsdf.cpp @@ -63,6 +63,21 @@ TSDFVolumeCPU::TSDFVolumeCPU(float _voxelSize, cv::Matx44f _pose, float _raycast : TSDFVolume(_voxelSize, _pose, _raycastStepFactor, _truncDist, _maxWeight, _resolution, zFirstMemOrder) { + int xdim, ydim, zdim; + if (zFirstMemOrder) + { + xdim = volResolution.z * volResolution.y; + ydim = volResolution.z; + zdim = 1; + } + else + { + xdim = 1; + ydim = volResolution.x; + zdim = volResolution.x * volResolution.y; + } + volStrides = Vec4i(xdim, ydim, zdim); + volume = Mat(1, volResolution.x * volResolution.y * volResolution.z, rawType()); reset(); @@ -99,272 +114,6 @@ TsdfVoxel TSDFVolumeCPU::at(const Vec3i& volumeIdx) const return volData[coordBase]; } - -struct IntegrateInvoker : ParallelLoopBody -{ - IntegrateInvoker(TSDFVolumeCPU& _volume, const Depth& _depth, const Intr& intrinsics, - const cv::Matx44f& cameraPose, float depthFactor, Mat _pixNorms) : - ParallelLoopBody(), - volume(_volume), - depth(_depth), - intr(intrinsics), - proj(intrinsics.makeProjector()), - vol2cam(Affine3f(cameraPose.inv()) * _volume.pose), - truncDistInv(1.f/_volume.truncDist), - dfac(1.f/depthFactor), - pixNorms(_pixNorms) - { - volDataStart = volume.volume.ptr(); - } - -#if USE_INTRINSICS - virtual void operator() (const Range& range) const override - { - // zStep == vol2cam*(Point3f(x, y, 1)*voxelSize) - basePt; - Point3f zStepPt = Point3f(vol2cam.matrix(0, 2), - vol2cam.matrix(1, 2), - vol2cam.matrix(2, 2))*volume.voxelSize; - - v_float32x4 zStep(zStepPt.x, zStepPt.y, zStepPt.z, 0); - v_float32x4 vfxy(proj.fx, proj.fy, 0.f, 0.f), vcxy(proj.cx, proj.cy, 0.f, 0.f); - const v_float32x4 upLimits = v_cvt_f32(v_int32x4(depth.cols-1, depth.rows-1, 0, 0)); - - for(int x = range.start; x < range.end; x++) - { - TsdfVoxel* volDataX = volDataStart + x*volume.volDims[0]; - for(int y = 0; y < volume.volResolution.y; y++) - { - TsdfVoxel* volDataY = volDataX + y*volume.volDims[1]; - // optimization of camSpace transformation (vector addition instead of matmul at each z) - Point3f basePt = vol2cam*(Point3f((float)x, (float)y, 0)*volume.voxelSize); - v_float32x4 camSpacePt(basePt.x, basePt.y, basePt.z, 0); - - int startZ, endZ; - if(abs(zStepPt.z) > 1e-5) - { - int baseZ = (int)(-basePt.z / zStepPt.z); - if(zStepPt.z > 0) - { - startZ = baseZ; - endZ = volume.volResolution.z; - } - else - { - startZ = 0; - endZ = baseZ; - } - } - else - { - if (basePt.z > 0) - { - startZ = 0; - endZ = volume.volResolution.z; - } - else - { - // z loop shouldn't be performed - startZ = endZ = 0; - } - } - startZ = max(0, startZ); - endZ = min(volume.volResolution.z, endZ); - for(int z = startZ; z < endZ; z++) - { - // optimization of the following: - //Point3f volPt = Point3f(x, y, z)*voxelSize; - //Point3f camSpacePt = vol2cam * volPt; - camSpacePt += zStep; - - float zCamSpace = v_reinterpret_as_f32(v_rotate_right<2>(v_reinterpret_as_u32(camSpacePt))).get0(); - if(zCamSpace <= 0.f) - continue; - - v_float32x4 camPixVec = camSpacePt/v_setall_f32(zCamSpace); - v_float32x4 projected = v_muladd(camPixVec, vfxy, vcxy); - // leave only first 2 lanes - projected = v_reinterpret_as_f32(v_reinterpret_as_u32(projected) & - v_uint32x4(0xFFFFFFFF, 0xFFFFFFFF, 0, 0)); - - depthType v; - // bilinearly interpolate depth at projected - { - const v_float32x4& pt = projected; - // check coords >= 0 and < imgSize - v_uint32x4 limits = v_reinterpret_as_u32(pt < v_setzero_f32()) | - v_reinterpret_as_u32(pt >= upLimits); - limits = limits | v_rotate_right<1>(limits); - if(limits.get0()) - continue; - - // xi, yi = floor(pt) - v_int32x4 ip = v_floor(pt); - v_int32x4 ipshift = ip; - int xi = ipshift.get0(); - ipshift = v_rotate_right<1>(ipshift); - int yi = ipshift.get0(); - - const depthType* row0 = depth[yi+0]; - const depthType* row1 = depth[yi+1]; - - // v001 = [v(xi + 0, yi + 0), v(xi + 1, yi + 0)] - v_float32x4 v001 = v_load_low(row0 + xi); - // v101 = [v(xi + 0, yi + 1), v(xi + 1, yi + 1)] - v_float32x4 v101 = v_load_low(row1 + xi); - - v_float32x4 vall = v_combine_low(v001, v101); - - // assume correct depth is positive - // don't fix missing data - if(v_check_all(vall > v_setzero_f32())) - { - v_float32x4 t = pt - v_cvt_f32(ip); - float tx = t.get0(); - t = v_reinterpret_as_f32(v_rotate_right<1>(v_reinterpret_as_u32(t))); - v_float32x4 ty = v_setall_f32(t.get0()); - // vx is y-interpolated between rows 0 and 1 - v_float32x4 vx = v001 + ty*(v101 - v001); - float v0 = vx.get0(); - vx = v_reinterpret_as_f32(v_rotate_right<1>(v_reinterpret_as_u32(vx))); - float v1 = vx.get0(); - v = v0 + tx*(v1 - v0); - } - else - continue; - } - - // norm(camPixVec) produces double which is too slow - int _u = (int) projected.get0(); - int _v = (int) v_rotate_right<1>(projected).get0(); - if (!(_u >= 0 && _u < depth.cols && _v >= 0 && _v < depth.rows)) - continue; - float pixNorm = pixNorms.at(_v, _u); - // float pixNorm = sqrt(v_reduce_sum(camPixVec*camPixVec)); - // difference between distances of point and of surface to camera - float sdf = pixNorm*(v*dfac - zCamSpace); - // possible alternative is: - // kftype sdf = norm(camSpacePt)*(v*dfac/camSpacePt.z - 1); - if(sdf >= -volume.truncDist) - { - TsdfType tsdf = floatToTsdf(fmin(1.f, sdf * truncDistInv)); - - TsdfVoxel& voxel = volDataY[z*volume.volDims[2]]; - WeightType& weight = voxel.weight; - TsdfType& value = voxel.tsdf; - - // update TSDF - value = floatToTsdf((tsdfToFloat(value)*weight+ tsdfToFloat(tsdf)) / (weight + 1)); - weight = (weight + 1) < volume.maxWeight ? (weight + 1) : volume.maxWeight; - } - } - } - } - } -#else - virtual void operator() (const Range& range) const override - { - for(int x = range.start; x < range.end; x++) - { - TsdfVoxel* volDataX = volDataStart + x*volume.volDims[0]; - for(int y = 0; y < volume.volResolution.y; y++) - { - TsdfVoxel* volDataY = volDataX+y*volume.volDims[1]; - // optimization of camSpace transformation (vector addition instead of matmul at each z) - Point3f basePt = vol2cam*(Point3f(float(x), float(y), 0.0f)*volume.voxelSize); - Point3f camSpacePt = basePt; - // zStep == vol2cam*(Point3f(x, y, 1)*voxelSize) - basePt; - // zStep == vol2cam*[Point3f(x, y, 1) - Point3f(x, y, 0)]*voxelSize - Point3f zStep = Point3f(vol2cam.matrix(0, 2), - vol2cam.matrix(1, 2), - vol2cam.matrix(2, 2))*volume.voxelSize; - int startZ, endZ; - if(abs(zStep.z) > 1e-5) - { - int baseZ = int(-basePt.z / zStep.z); - if(zStep.z > 0) - { - startZ = baseZ; - endZ = volume.volResolution.z; - } - else - { - startZ = 0; - endZ = baseZ; - } - } - else - { - if(basePt.z > 0) - { - startZ = 0; - endZ = volume.volResolution.z; - } - else - { - // z loop shouldn't be performed - startZ = endZ = 0; - } - } - startZ = max(0, startZ); - endZ = min(volume.volResolution.z, endZ); - - for (int z = startZ; z < endZ; z++) - { - // optimization of the following: - //Point3f volPt = Point3f(x, y, z)*volume.voxelSize; - //Point3f camSpacePt = vol2cam * volPt; - - camSpacePt += zStep; - if(camSpacePt.z <= 0) - continue; - - Point3f camPixVec; - Point2f projected = proj(camSpacePt, camPixVec); - - depthType v = bilinearDepth(depth, projected); - if (v == 0) { - continue; - } - - int _u = projected.x; - int _v = projected.y; - if (!(_u >= 0 && _u < depth.cols && _v >= 0 && _v < depth.rows)) - continue; - float pixNorm = pixNorms.at(_v, _u); - - // difference between distances of point and of surface to camera - float sdf = pixNorm*(v*dfac - camSpacePt.z); - // possible alternative is: - // kftype sdf = norm(camSpacePt)*(v*dfac/camSpacePt.z - 1); - if(sdf >= -volume.truncDist) - { - TsdfType tsdf = floatToTsdf(fmin(1.f, sdf * truncDistInv)); - - TsdfVoxel& voxel = volDataY[z*volume.volDims[2]]; - WeightType& weight = voxel.weight; - TsdfType& value = voxel.tsdf; - - // update TSDF - value = floatToTsdf((tsdfToFloat(value)*weight+ tsdfToFloat(tsdf)) / (weight + 1)); - weight = min(int(weight + 1), int(volume.maxWeight)); - } - } - } - } - } -#endif - - TSDFVolumeCPU& volume; - const Depth& depth; - const Intr& intr; - const Intr::Projector proj; - const Affine3f vol2cam; - const float truncDistInv; - const float dfac; - TsdfVoxel* volDataStart; - Mat pixNorms; -}; - // use depth instead of distance (optimization) void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44f& cameraPose, const Intr& intrinsics, const int frameId) @@ -384,9 +133,8 @@ void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44 pixNorms = preCalculationPixNorm(depth, intrinsics); } - IntegrateInvoker ii(*this, depth, intrinsics, cameraPose, depthFactor, pixNorms); - Range range(0, volResolution.x); - parallel_for_(range, ii); + integrateVolumeUnit(truncDist, voxelSize, maxWeight, (this->pose).matrix, volResolution.x, volStrides, depth, + depthFactor, cameraPose, intrinsics, pixNorms, volume); } #if USE_INTRINSICS diff --git a/modules/rgbd/src/tsdf.hpp b/modules/rgbd/src/tsdf.hpp index cd38ee80b69..009c4a8466c 100644 --- a/modules/rgbd/src/tsdf.hpp +++ b/modules/rgbd/src/tsdf.hpp @@ -76,6 +76,7 @@ class TSDFVolumeCPU : public TSDFVolume float interpolateVoxel(const v_float32x4& p) const; v_float32x4 getNormalVoxel(const v_float32x4& p) const; #endif + Vec4i volStrides; Vec6f frameParams; Mat pixNorms; // See zFirstMemOrder arg of parent class constructor From 5b640a53f1961461d3810ac15e2e1ab9303dd187 Mon Sep 17 00:00:00 2001 From: Rob Timpe Date: Wed, 21 Oct 2020 15:51:46 -0700 Subject: [PATCH 07/29] [moved from opencv] Fix errors when building with cuda stubs Fixes two errors when building with the options WITH_CUDA=ON and BUILD_CUDA_STUBS=ON on a machine without CUDA. In the cudaarithm module, make sure cuda_runtime.h only gets included when CUDA is installed. In the stitching module, don't assume that cuda is present just because cudaarithm and cudawarping are present (as is the case when building with the above options). original commit: https://github.com/opencv/opencv/commit/22ee5c0c4db9111780c6d34eb2a3c85e0f4046ff --- modules/cudaarithm/src/lut.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/cudaarithm/src/lut.cpp b/modules/cudaarithm/src/lut.cpp index a4b4e02650a..5ef28360176 100644 --- a/modules/cudaarithm/src/lut.cpp +++ b/modules/cudaarithm/src/lut.cpp @@ -4,8 +4,6 @@ #include "precomp.hpp" -#include "lut.hpp" - using namespace cv; using namespace cv::cuda; @@ -15,6 +13,9 @@ Ptr cv::cuda::createLookUpTable(InputArray) { throw_no_cuda(); retu #else /* !defined (HAVE_CUDA) || defined (CUDA_DISABLER) */ +// lut.hpp includes cuda_runtime.h and can only be included when we have CUDA +#include "lut.hpp" + Ptr cv::cuda::createLookUpTable(InputArray lut) { return makePtr(lut); From d8197c6ad64468c8b199dcd2343184a51a366f92 Mon Sep 17 00:00:00 2001 From: TT <45615081+tsukada-cs@users.noreply.github.com> Date: Fri, 23 Oct 2020 02:03:11 +0900 Subject: [PATCH 08/29] Merge pull request #2716 from tsukada-cs:feature/fld-is_edge-option Added edge input feature to fast_line_detector * add is_edge option on fast_line_detector * Fixed function declarations * Added is_edge test * Add input_edge option to createFastLineDetector(). Deleted is_edge option from detect(). * Fixed the Docs issue (whitespace opencv_contrib). * Fixed Docs issue. * Added assertion check on canny_aperture_size = 0. Removed the input_edge option from createFastLineDetector(). * fixed Docs and coding style --- .../opencv2/ximgproc/fast_line_detector.hpp | 5 ++-- modules/ximgproc/samples/fld_lines.cpp | 5 ++-- modules/ximgproc/src/fast_line_detector.cpp | 23 +++++++++------ modules/ximgproc/test/test_fld.cpp | 28 +++++++++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/modules/ximgproc/include/opencv2/ximgproc/fast_line_detector.hpp b/modules/ximgproc/include/opencv2/ximgproc/fast_line_detector.hpp index 1df555865eb..4a33148be28 100644 --- a/modules/ximgproc/include/opencv2/ximgproc/fast_line_detector.hpp +++ b/modules/ximgproc/include/opencv2/ximgproc/fast_line_detector.hpp @@ -65,8 +65,9 @@ class CV_EXPORTS_W FastLineDetector : public Algorithm hysteresis procedure in Canny() @param _canny_th2 50 - Second threshold for hysteresis procedure in Canny() -@param _canny_aperture_size 3 - Aperturesize for the sobel - operator in Canny() +@param _canny_aperture_size 3 - Aperturesize for the sobel operator in Canny(). + If zero, Canny() is not applied and the input + image is taken as an edge image. @param _do_merge false - If true, incremental merging of segments will be perfomred */ diff --git a/modules/ximgproc/samples/fld_lines.cpp b/modules/ximgproc/samples/fld_lines.cpp index 62a2d48cb39..fdc148bd033 100644 --- a/modules/ximgproc/samples/fld_lines.cpp +++ b/modules/ximgproc/samples/fld_lines.cpp @@ -37,8 +37,9 @@ int main(int argc, char** argv) // hysteresis procedure in Canny() // canny_th2 50 - Second threshold for // hysteresis procedure in Canny() - // canny_aperture_size 3 - Aperturesize for the sobel - // operator in Canny() + // canny_aperture_size 3 - Aperturesize for the sobel operator in Canny(). + // If zero, Canny() is not applied and the input + // image is taken as an edge image. // do_merge false - If true, incremental merging of segments // will be perfomred int length_threshold = 10; diff --git a/modules/ximgproc/src/fast_line_detector.cpp b/modules/ximgproc/src/fast_line_detector.cpp index 33560184d2a..4217d9b33c5 100644 --- a/modules/ximgproc/src/fast_line_detector.cpp +++ b/modules/ximgproc/src/fast_line_detector.cpp @@ -29,10 +29,11 @@ class FastLineDetectorImpl : public FastLineDetector * _ hysteresis procedure in Canny() * @param _canny_th2 50 - Second threshold for * _ hysteresis procedure in Canny() - * @param _canny_aperture_size 3 - Aperturesize for the sobel - * _ operator in Canny() + * @param _canny_aperture_size 3 - Aperturesize for the sobel operator in Canny(). + * If zero, Canny() is not applied and the input + * image is taken as an edge image. * @param _do_merge false - If true, incremental merging of segments - will be perfomred + * will be performed */ FastLineDetectorImpl(int _length_threshold = 10, float _distance_threshold = 1.414213562f, double _canny_th1 = 50.0, double _canny_th2 = 50.0, int _canny_aperture_size = 3, @@ -80,7 +81,7 @@ class FastLineDetectorImpl : public FastLineDetector double distPointLine(const Mat& p, Mat& l); - void extractSegments(const std::vector& points, std::vector& segments ); + void extractSegments(const std::vector& points, std::vector& segments); void lineDetection(const Mat& src, std::vector& segments_all); @@ -113,7 +114,7 @@ FastLineDetectorImpl::FastLineDetectorImpl(int _length_threshold, float _distanc canny_th1(_canny_th1), canny_th2(_canny_th2), canny_aperture_size(_canny_aperture_size), do_merge(_do_merge) { CV_Assert(_length_threshold > 0 && _distance_threshold > 0 && - _canny_th1 > 0 && _canny_th2 > 0 && _canny_aperture_size > 0); + _canny_th1 > 0 && _canny_th2 > 0 && _canny_aperture_size >= 0); } void FastLineDetectorImpl::detect(InputArray _image, OutputArray _lines) @@ -344,7 +345,7 @@ template pt = T(pt_tmp); } -void FastLineDetectorImpl::extractSegments(const std::vector& points, std::vector& segments ) +void FastLineDetectorImpl::extractSegments(const std::vector& points, std::vector& segments) { bool is_line; @@ -544,8 +545,14 @@ void FastLineDetectorImpl::lineDetection(const Mat& src, std::vector& s std::vector points; std::vector segments, segments_tmp; Mat canny; - Canny(src, canny, canny_th1, canny_th2, canny_aperture_size); - + if (canny_aperture_size == 0) + { + canny = src; + } + else + { + Canny(src, canny, canny_th1, canny_th2, canny_aperture_size); + } canny.colRange(0,6).rowRange(0,6) = 0; canny.colRange(src.cols-5,src.cols).rowRange(src.rows-5,src.rows) = 0; diff --git a/modules/ximgproc/test/test_fld.cpp b/modules/ximgproc/test/test_fld.cpp index 80f877648bc..e4dfc9f4abd 100644 --- a/modules/ximgproc/test/test_fld.cpp +++ b/modules/ximgproc/test/test_fld.cpp @@ -23,6 +23,7 @@ class FLDBase : public testing::Test void GenerateWhiteNoise(Mat& image); void GenerateConstColor(Mat& image); void GenerateLines(Mat& image, const unsigned int numLines); + void GenerateEdgeLines(Mat& image, const unsigned int numLines); void GenerateBrokenLines(Mat& image, const unsigned int numLines); void GenerateRotatedRect(Mat& image); virtual void SetUp(); @@ -47,6 +48,7 @@ void FLDBase::GenerateConstColor(Mat& image) image = Mat(img_size, CV_8UC1, Scalar::all(rng.uniform(0, 256))); } + void FLDBase::GenerateLines(Mat& image, const unsigned int numLines) { image = Mat(img_size, CV_8UC1, Scalar::all(rng.uniform(0, 128))); @@ -60,6 +62,19 @@ void FLDBase::GenerateLines(Mat& image, const unsigned int numLines) } } +void FLDBase::GenerateEdgeLines(Mat& image, const unsigned int numLines) +{ + image = Mat(img_size, CV_8UC1, Scalar::all(0)); + + for(unsigned int i = 0; i < numLines; ++i) + { + int y = rng.uniform(10, img_size.width - 10); + Point p1(y, 10); + Point p2(y, img_size.height - 10); + line(image, p1, p2, Scalar(255), 1); + } +} + void FLDBase::GenerateBrokenLines(Mat& image, const unsigned int numLines) { image = Mat(img_size, CV_8UC1, Scalar::all(rng.uniform(0, 128))); @@ -145,6 +160,19 @@ TEST_F(ximgproc_FLD, lines) ASSERT_EQ(EPOCHS, passedtests); } +TEST_F(ximgproc_FLD, edgeLines) +{ + for (int i = 0; i < EPOCHS; ++i) + { + const unsigned int numOfLines = 1; + GenerateEdgeLines(test_image, numOfLines); + Ptr detector = createFastLineDetector(10, 1.414213562f, 50, 50, 0); + detector->detect(test_image, lines); + if(numOfLines == lines.size()) ++passedtests; + } + ASSERT_EQ(EPOCHS, passedtests); +} + TEST_F(ximgproc_FLD, mergeLines) { for (int i = 0; i < EPOCHS; ++i) From 10bfd2dc51ea21828b0d9a0fdce8e44c055e6696 Mon Sep 17 00:00:00 2001 From: DumDereDum <46279571+DumDereDum@users.noreply.github.com> Date: Mon, 26 Oct 2020 22:54:53 +0300 Subject: [PATCH 09/29] Merge pull request #2725 from DumDereDum:integrateVolumeUnit_fix integrateVolumeUnit fix * replace int with Point3i at integrateVolumeUnit signature * Update hash_tsdf.hpp * Update hash_tsdf.cpp * Update hash_tsdf.cpp * error fix * minor fix --- modules/rgbd/src/hash_tsdf.cpp | 3 ++- modules/rgbd/src/tsdf.cpp | 2 +- modules/rgbd/src/tsdf_functions.cpp | 20 ++++++++++---------- modules/rgbd/src/tsdf_functions.hpp | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/modules/rgbd/src/hash_tsdf.cpp b/modules/rgbd/src/hash_tsdf.cpp index 3c5d2d5d43d..35838352d43 100644 --- a/modules/rgbd/src/hash_tsdf.cpp +++ b/modules/rgbd/src/hash_tsdf.cpp @@ -216,7 +216,8 @@ void HashTSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Ma if (volumeUnit.isActive) { //! The volume unit should already be added into the Volume from the allocator - integrateVolumeUnit(truncDist, voxelSize, maxWeight, volumeUnit.pose, volumeUnitResolution, volStrides, depth, + integrateVolumeUnit(truncDist, voxelSize, maxWeight, volumeUnit.pose, + Point3i(volumeUnitResolution, volumeUnitResolution, volumeUnitResolution), volStrides, depth, depthFactor, cameraPose, intrinsics, pixNorms, volUnitsData.row(volumeUnit.index)); //! Ensure all active volumeUnits are set to inactive for next integration diff --git a/modules/rgbd/src/tsdf.cpp b/modules/rgbd/src/tsdf.cpp index 6e05bebb1d1..1e8704170f4 100644 --- a/modules/rgbd/src/tsdf.cpp +++ b/modules/rgbd/src/tsdf.cpp @@ -133,7 +133,7 @@ void TSDFVolumeCPU::integrate(InputArray _depth, float depthFactor, const Matx44 pixNorms = preCalculationPixNorm(depth, intrinsics); } - integrateVolumeUnit(truncDist, voxelSize, maxWeight, (this->pose).matrix, volResolution.x, volStrides, depth, + integrateVolumeUnit(truncDist, voxelSize, maxWeight, (this->pose).matrix, volResolution, volStrides, depth, depthFactor, cameraPose, intrinsics, pixNorms, volume); } diff --git a/modules/rgbd/src/tsdf_functions.cpp b/modules/rgbd/src/tsdf_functions.cpp index 3eb27742c1e..ff40dce248d 100644 --- a/modules/rgbd/src/tsdf_functions.cpp +++ b/modules/rgbd/src/tsdf_functions.cpp @@ -111,7 +111,7 @@ depthType bilinearDepth(const Depth& m, cv::Point2f pt) void integrateVolumeUnit( float truncDist, float voxelSize, int maxWeight, - cv::Matx44f _pose, int volResolution, Vec4i volStrides, + cv::Matx44f _pose, Point3i volResolution, Vec4i volStrides, InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, const cv::kinfu::Intr& intrinsics, InputArray _pixNorms, InputArray _volume) { @@ -122,7 +122,7 @@ void integrateVolumeUnit( cv::Affine3f vpose(_pose); Depth depth = _depth.getMat(); - Range integrateRange(0, volResolution); + Range integrateRange(0, volResolution.x); Mat volume = _volume.getMat(); Mat pixNorms = _pixNorms.getMat(); @@ -147,7 +147,7 @@ void integrateVolumeUnit( for (int x = range.start; x < range.end; x++) { TsdfVoxel* volDataX = volDataStart + x * volStrides[0]; - for (int y = 0; y < volResolution; y++) + for (int y = 0; y < volResolution.y; y++) { TsdfVoxel* volDataY = volDataX + y * volStrides[1]; // optimization of camSpace transformation (vector addition instead of matmul at each z) @@ -161,7 +161,7 @@ void integrateVolumeUnit( if (zStepPt.z > 0) { startZ = baseZ; - endZ = volResolution; + endZ = volResolution.z; } else { @@ -174,7 +174,7 @@ void integrateVolumeUnit( if (basePt.z > 0) { startZ = 0; - endZ = volResolution; + endZ = volResolution.z; } else { @@ -183,7 +183,7 @@ void integrateVolumeUnit( } } startZ = max(0, startZ); - endZ = min(int(volResolution), endZ); + endZ = min(int(volResolution.z), endZ); for (int z = startZ; z < endZ; z++) { // optimization of the following: @@ -281,7 +281,7 @@ void integrateVolumeUnit( for (int x = range.start; x < range.end; x++) { TsdfVoxel* volDataX = volDataStart + x * volStrides[0]; - for (int y = 0; y < volResolution; y++) + for (int y = 0; y < volResolution.y; y++) { TsdfVoxel* volDataY = volDataX + y * volStrides[1]; // optimization of camSpace transformation (vector addition instead of matmul at each z) @@ -299,7 +299,7 @@ void integrateVolumeUnit( if (zStep.z > 0) { startZ = baseZ; - endZ = volResolution; + endZ = volResolution.z; } else { @@ -312,7 +312,7 @@ void integrateVolumeUnit( if (basePt.z > 0) { startZ = 0; - endZ = volResolution; + endZ = volResolution.z; } else { @@ -321,7 +321,7 @@ void integrateVolumeUnit( } } startZ = max(0, startZ); - endZ = min(int(volResolution), endZ); + endZ = min(int(volResolution.z), endZ); for (int z = startZ; z < endZ; z++) { diff --git a/modules/rgbd/src/tsdf_functions.hpp b/modules/rgbd/src/tsdf_functions.hpp index 28f776d752f..6d86595118f 100644 --- a/modules/rgbd/src/tsdf_functions.hpp +++ b/modules/rgbd/src/tsdf_functions.hpp @@ -39,7 +39,7 @@ depthType bilinearDepth(const Depth& m, cv::Point2f pt); void integrateVolumeUnit( float truncDist, float voxelSize, int maxWeight, - cv::Matx44f _pose, int volResolution, Vec4i volStrides, + cv::Matx44f _pose, Point3i volResolution, Vec4i volStrides, InputArray _depth, float depthFactor, const cv::Matx44f& cameraPose, const cv::kinfu::Intr& intrinsics, InputArray _pixNorms, InputArray _volume); From 21591028fb8d7329c4362086b99727460091f091 Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Tue, 27 Oct 2020 11:25:22 +0000 Subject: [PATCH 10/29] optflow(rlof): fix uninitialized variable --- modules/optflow/src/rlof/berlof_invoker.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/optflow/src/rlof/berlof_invoker.hpp b/modules/optflow/src/rlof/berlof_invoker.hpp index 36e94f73644..ac866fe5344 100644 --- a/modules/optflow/src/rlof/berlof_invoker.hpp +++ b/modules/optflow/src/rlof/berlof_invoker.hpp @@ -187,7 +187,6 @@ class TrackerInvoker : public cv::ParallelLoopBody cv::Size _winSize = winSize; float MEstimatorScale = 1; int buffIdx = 0; - float c[8]; cv::Mat GMc0, GMc1, GMc2, GMc3; cv::Vec2f Mc0, Mc1, Mc2, Mc3; int noIteration = 0; @@ -572,6 +571,7 @@ class TrackerInvoker : public cv::ParallelLoopBody Mc3[0] = _b0[3]; Mc3[1] = _b1[3]; + float c[8] = {}; c[0] = -Mc0[0]; c[1] = -Mc1[0]; c[2] = -Mc2[0]; @@ -830,7 +830,6 @@ class TrackerInvoker : public cv::ParallelLoopBody int j; float MEstimatorScale = 1; int buffIdx = 0; - float c[8]; cv::Mat GMc0, GMc1, GMc2, GMc3; cv::Vec4f Mc0, Mc1, Mc2, Mc3; int noIteration = 0; @@ -1355,6 +1354,7 @@ class TrackerInvoker : public cv::ParallelLoopBody Mc3[3] = -_b3[3]; // + float c[8] = {}; c[0] = -Mc0[0]; c[1] = -Mc1[0]; c[2] = -Mc2[0]; @@ -1620,7 +1620,6 @@ class TrackerInvoker : public cv::ParallelLoopBody nextPt += halfWin; Point2f prevDelta(0,0); - float c[8]; for( j = 0; j < criteria.maxCount; j++ ) { cv::Point2f delta; @@ -1629,6 +1628,7 @@ class TrackerInvoker : public cv::ParallelLoopBody b = nextPt.y - cvFloor(nextPt.y); float ab = a * b; + float c[8] = {}; if( (inextPt.x != cvFloor(nextPt.x) || inextPt.y != cvFloor(nextPt.y) || j == 0)) { @@ -1996,7 +1996,6 @@ namespace radial { cv::Point2f backUpGain = gainVec; cv::Size _winSize = winSize; int j; - float c[8]; cv::Mat GMc0, GMc1, GMc2, GMc3; cv::Vec4f Mc0, Mc1, Mc2, Mc3; int noIteration = 0; @@ -2359,6 +2358,7 @@ namespace radial { Mc3[3] = -_b3[3]; // + float c[8] = {}; c[0] = -Mc0[0]; c[1] = -Mc1[0]; c[2] = -Mc2[0]; From 65ca496a99a228d6da62d43a929fe8915c16a47a Mon Sep 17 00:00:00 2001 From: Igor Murzov Date: Tue, 27 Oct 2020 23:42:56 +0300 Subject: [PATCH 11/29] Fix build failure in rgbd's test Fixes the following compiler error: rgbd/test/test_dynafu.cpp:44:17: error: 'Params' is not a member of 'cv::dynafu' 44 | Ptr params; | ^~~~~~ --- modules/rgbd/test/test_dynafu.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/rgbd/test/test_dynafu.cpp b/modules/rgbd/test/test_dynafu.cpp index 78a1a8fd92f..1ee102e6e0f 100644 --- a/modules/rgbd/test/test_dynafu.cpp +++ b/modules/rgbd/test/test_dynafu.cpp @@ -41,11 +41,11 @@ static const bool display = false; void flyTest(bool hiDense, bool inequal) { - Ptr params; + Ptr params; if(hiDense) - params = dynafu::Params::defaultParams(); + params = kinfu::Params::defaultParams(); else - params = dynafu::Params::coarseParams(); + params = kinfu::Params::coarseParams(); if(inequal) { From 5b05f5c36fef0a3dc3c275b233db5402fb028a3f Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Sat, 31 Oct 2020 12:57:41 +0000 Subject: [PATCH 12/29] sfm: fix memory leak --- modules/sfm/src/libmv_capi.h | 27 ++++++++++++++------------- modules/sfm/src/simple_pipeline.cpp | 25 ++++++++++++++----------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/modules/sfm/src/libmv_capi.h b/modules/sfm/src/libmv_capi.h index cf2e5daf75a..3c39c2b5c71 100644 --- a/modules/sfm/src/libmv_capi.h +++ b/modules/sfm/src/libmv_capi.h @@ -75,7 +75,7 @@ struct libmv_Reconstruction { EuclideanReconstruction reconstruction; /* Used for per-track average error calculation after reconstruction */ Tracks tracks; - CameraIntrinsics *intrinsics; + std::shared_ptr intrinsics; double error; bool is_valid; }; @@ -252,21 +252,22 @@ static void libmv_cameraIntrinsicsFillFromOptions( * options values. */ -static CameraIntrinsics* libmv_cameraIntrinsicsCreateFromOptions( +static +std::shared_ptr libmv_cameraIntrinsicsCreateFromOptions( const libmv_CameraIntrinsicsOptions* camera_intrinsics_options) { - CameraIntrinsics *camera_intrinsics = NULL; + std::shared_ptr camera_intrinsics; switch (camera_intrinsics_options->distortion_model) { case SFM_DISTORTION_MODEL_POLYNOMIAL: - camera_intrinsics = new PolynomialCameraIntrinsics(); + camera_intrinsics = std::make_shared(); break; case SFM_DISTORTION_MODEL_DIVISION: - camera_intrinsics = new DivisionCameraIntrinsics(); + camera_intrinsics = std::make_shared(); break; default: assert(!"Unknown distortion model"); } libmv_cameraIntrinsicsFillFromOptions(camera_intrinsics_options, - camera_intrinsics); + camera_intrinsics.get()); return camera_intrinsics; } @@ -361,19 +362,19 @@ static void finishReconstruction( /* Perform the complete reconstruction process */ -static libmv_Reconstruction *libmv_solveReconstruction( +static +std::shared_ptr libmv_solveReconstruction( const Tracks &libmv_tracks, const libmv_CameraIntrinsicsOptions* libmv_camera_intrinsics_options, libmv_ReconstructionOptions* libmv_reconstruction_options) { - libmv_Reconstruction *libmv_reconstruction = - new libmv_Reconstruction(); + std::shared_ptr libmv_reconstruction = std::make_shared(); Tracks tracks = libmv_tracks; EuclideanReconstruction &reconstruction = libmv_reconstruction->reconstruction; /* Retrieve reconstruction options from C-API to libmv API. */ - CameraIntrinsics *camera_intrinsics; + std::shared_ptr camera_intrinsics; camera_intrinsics = libmv_reconstruction->intrinsics = libmv_cameraIntrinsicsCreateFromOptions(libmv_camera_intrinsics_options); @@ -426,7 +427,7 @@ static libmv_Reconstruction *libmv_solveReconstruction( libmv_reconstruction_options->refine_intrinsics, libmv::BUNDLE_NO_CONSTRAINTS, &reconstruction, - camera_intrinsics); + camera_intrinsics.get()); } /* Set reconstruction scale to unity. */ @@ -434,10 +435,10 @@ static libmv_Reconstruction *libmv_solveReconstruction( finishReconstruction(tracks, *camera_intrinsics, - libmv_reconstruction); + libmv_reconstruction.get()); libmv_reconstruction->is_valid = true; - return (libmv_Reconstruction *) libmv_reconstruction; + return libmv_reconstruction; } #endif diff --git a/modules/sfm/src/simple_pipeline.cpp b/modules/sfm/src/simple_pipeline.cpp index a0816108452..476b17e8794 100644 --- a/modules/sfm/src/simple_pipeline.cpp +++ b/modules/sfm/src/simple_pipeline.cpp @@ -118,7 +118,8 @@ parser_2D_tracks( const libmv::Matches &matches, libmv::Tracks &tracks ) * reconstruction pipeline. */ -static libmv_Reconstruction *libmv_solveReconstructionImpl( +static +std::shared_ptr libmv_solveReconstructionImpl( const std::vector &images, const libmv_CameraIntrinsicsOptions* libmv_camera_intrinsics_options, libmv_ReconstructionOptions* libmv_reconstruction_options) @@ -182,9 +183,10 @@ class SFMLibmvReconstructionImpl : public T // Perform reconstruction libmv_reconstruction_ = - *libmv_solveReconstruction(tracks, + libmv_solveReconstruction(tracks, &libmv_camera_intrinsics_options_, &libmv_reconstruction_options_); + CV_Assert(libmv_reconstruction_); } virtual void run(InputArrayOfArrays points2d, InputOutputArray K, OutputArray Rs, @@ -216,9 +218,10 @@ class SFMLibmvReconstructionImpl : public T // Perform reconstruction libmv_reconstruction_ = - *libmv_solveReconstructionImpl(images, + libmv_solveReconstructionImpl(images, &libmv_camera_intrinsics_options_, &libmv_reconstruction_options_); + CV_Assert(libmv_reconstruction_); } @@ -232,12 +235,12 @@ class SFMLibmvReconstructionImpl : public T extractLibmvReconstructionData(K, Rs, Ts, points3d); } - virtual double getError() const { return libmv_reconstruction_.error; } + virtual double getError() const { return libmv_reconstruction_->error; } virtual void getPoints(OutputArray points3d) { const size_t n_points = - libmv_reconstruction_.reconstruction.AllPoints().size(); + libmv_reconstruction_->reconstruction.AllPoints().size(); points3d.create(n_points, 1, CV_64F); @@ -246,7 +249,7 @@ class SFMLibmvReconstructionImpl : public T { for ( int j = 0; j < 3; ++j ) point3d[j] = - libmv_reconstruction_.reconstruction.AllPoints()[i].X[j]; + libmv_reconstruction_->reconstruction.AllPoints()[i].X[j]; Mat(point3d).copyTo(points3d.getMatRef(i)); } @@ -254,14 +257,14 @@ class SFMLibmvReconstructionImpl : public T virtual cv::Mat getIntrinsics() const { Mat K; - eigen2cv(libmv_reconstruction_.intrinsics->K(), K); + eigen2cv(libmv_reconstruction_->intrinsics->K(), K); return K; } virtual void getCameras(OutputArray Rs, OutputArray Ts) { const size_t n_views = - libmv_reconstruction_.reconstruction.AllCameras().size(); + libmv_reconstruction_->reconstruction.AllCameras().size(); Rs.create(n_views, 1, CV_64F); Ts.create(n_views, 1, CV_64F); @@ -270,8 +273,8 @@ class SFMLibmvReconstructionImpl : public T Vec3d t; for(size_t i = 0; i < n_views; ++i) { - eigen2cv(libmv_reconstruction_.reconstruction.AllCameras()[i].R, R); - eigen2cv(libmv_reconstruction_.reconstruction.AllCameras()[i].t, t); + eigen2cv(libmv_reconstruction_->reconstruction.AllCameras()[i].R, R); + eigen2cv(libmv_reconstruction_->reconstruction.AllCameras()[i].t, t); Mat(R).copyTo(Rs.getMatRef(i)); Mat(t).copyTo(Ts.getMatRef(i)); } @@ -300,7 +303,7 @@ class SFMLibmvReconstructionImpl : public T getIntrinsics().copyTo(K.getMat()); } - libmv_Reconstruction libmv_reconstruction_; + std::shared_ptr libmv_reconstruction_; libmv_ReconstructionOptions libmv_reconstruction_options_; libmv_CameraIntrinsicsOptions libmv_camera_intrinsics_options_; }; From 23ee62a19b7a3e50d6dbf295359d8b1aff2e03fd Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Thu, 29 Oct 2020 23:39:55 +0000 Subject: [PATCH 13/29] cmake: support Ceres 2.0.0 --- modules/rgbd/CMakeLists.txt | 8 +- modules/sfm/CMakeLists.txt | 90 ++++++++++++------- .../sfm/cmake/checks/check_glog_gflags.cpp | 7 ++ .../sfm/src/libmv_light/libmv/base/vector.h | 7 ++ .../libmv/multiview/CMakeLists.txt | 3 + .../libmv/multiview/fundamental.cc | 2 +- .../libmv_light/libmv/multiview/homography.cc | 2 +- .../libmv/simple_pipeline/bundle.cc | 2 +- .../libmv/simple_pipeline/intersect.cc | 2 +- .../libmv/simple_pipeline/tracks.cc | 4 - .../libmv/simple_pipeline/tracks.h | 2 +- 11 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 modules/sfm/cmake/checks/check_glog_gflags.cpp diff --git a/modules/rgbd/CMakeLists.txt b/modules/rgbd/CMakeLists.txt index 247c788a9c1..143cdf913af 100644 --- a/modules/rgbd/CMakeLists.txt +++ b/modules/rgbd/CMakeLists.txt @@ -2,10 +2,14 @@ set(the_description "RGBD algorithms") find_package(Ceres QUIET) ocv_define_module(rgbd opencv_core opencv_calib3d opencv_imgproc OPTIONAL opencv_viz WRAP python) -ocv_target_link_libraries(${the_module} ${CERES_LIBRARIES}) if(Ceres_FOUND) ocv_target_compile_definitions(${the_module} PUBLIC CERES_FOUND) + ocv_target_link_libraries(${the_module} ${CERES_LIBRARIES}) + if(Ceres_VERSION VERSION_LESS 2.0.0) + ocv_include_directories("${CERES_INCLUDE_DIRS}") + endif() + add_definitions(/DGLOG_NO_ABBREVIATED_SEVERITIES) # avoid ERROR macro conflict in glog (ceres dependency) else() - message(STATUS "CERES support is disabled. Ceres Solver is Required for Posegraph optimization") + message(STATUS "rgbd: CERES support is disabled. Ceres Solver is Required for Posegraph optimization") endif() diff --git a/modules/sfm/CMakeLists.txt b/modules/sfm/CMakeLists.txt index 53a8d4378a9..045a1fe6e45 100644 --- a/modules/sfm/CMakeLists.txt +++ b/modules/sfm/CMakeLists.txt @@ -3,23 +3,56 @@ set(the_description "SFM algorithms") ### LIBMV LIGHT EXTERNAL DEPENDENCIES ### -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") -find_package(Gflags QUIET) + find_package(Ceres QUIET) -if(NOT Ceres_FOUND) # Looks like Ceres find glog on the own, so separate search isn't necessary + +if(NOT Gflags_FOUND) # Ceres find gflags on the own, so separate search isn't necessary + find_package(Gflags QUIET) +endif() +if(NOT Glog_FOUND) # Ceres find glog on the own, so separate search isn't necessary find_package(Glog QUIET) endif() -if((gflags_FOUND OR GFLAGS_FOUND OR GFLAGS_INCLUDE_DIRS) AND (glog_FOUND OR GLOG_FOUND OR GLOG_INCLUDE_DIRS)) - set(_fname "${CMAKE_CURRENT_BINARY_DIR}/test_sfm_deps.cpp") - file(WRITE "${_fname}" "#include \n#include \nint main() { (void)(0); return 0; }\n") - try_compile(SFM_DEPS_OK "${CMAKE_BINARY_DIR}" "${_fname}" - CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:STRING=${GLOG_INCLUDE_DIRS};${GFLAGS_INCLUDE_DIRS}" - LINK_LIBRARIES ${GLOG_LIBRARIES} ${GFLAGS_LIBRARIES} - OUTPUT_VARIABLE OUTPUT - ) - file(REMOVE "${_fname}") - message(STATUS "Checking SFM deps... ${SFM_DEPS_OK}") +if(NOT Gflags_FOUND OR NOT Glog_FOUND) + # try local search scripts + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + if(NOT Gflags_FOUND) + find_package(Gflags QUIET) + endif() + if(NOT Glog_FOUND) + find_package(Glog QUIET) + endif() +endif() + +if(NOT DEFINED GFLAGS_INCLUDE_DIRS AND DEFINED GFLAGS_INCLUDE_DIR) + set(GFLAGS_INCLUDE_DIRS "${GFLAGS_INCLUDE_DIR}") +endif() +if(NOT DEFINED GLOG_INCLUDE_DIRS AND DEFINED GLOG_INCLUDE_DIR) + set(GLOG_INCLUDE_DIRS "${GLOG_INCLUDE_DIR}") +endif() + +if((gflags_FOUND OR Gflags_FOUND OR GFLAGS_FOUND OR GFLAGS_INCLUDE_DIRS) AND (glog_FOUND OR Glog_FOUND OR GLOG_FOUND OR GLOG_INCLUDE_DIRS)) + set(__cache_key "${GLOG_INCLUDE_DIRS} ~ ${GFLAGS_INCLUDE_DIRS} ~ ${GLOG_LIBRARIES} ~ ${GFLAGS_LIBRARIES}") + if(NOT DEFINED SFM_GLOG_GFLAGS_TEST_CACHE_KEY OR NOT (SFM_GLOG_GFLAGS_TEST_CACHE_KEY STREQUAL __cache_key)) + set(__fname "${CMAKE_CURRENT_LIST_DIR}/cmake/checks/check_glog_gflags.cpp") + try_compile( + SFM_GLOG_GFLAGS_TEST "${CMAKE_BINARY_DIR}" "${__fname}" + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:STRING=${GLOG_INCLUDE_DIRS};${GFLAGS_INCLUDE_DIRS}" + LINK_LIBRARIES ${GLOG_LIBRARIES} ${GFLAGS_LIBRARIES} + OUTPUT_VARIABLE __output + ) + if(NOT SFM_GLOG_GFLAGS_TEST) + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log + "Failed compilation check: ${__fname}\n" + "${__output}\n\n" + ) + endif() + set(SFM_GLOG_GFLAGS_TEST "${SFM_GLOG_GFLAGS_TEST}" CACHE INTERNAL "") + set(SFM_GLOG_GFLAGS_TEST_CACHE_KEY "${__cache_key}" CACHE INTERNAL "") + message(STATUS "Checking SFM glog/gflags deps... ${SFM_GLOG_GFLAGS_TEST}") + endif() + unset(__cache_key) + set(SFM_DEPS_OK "${SFM_GLOG_GFLAGS_TEST}") else() set(SFM_DEPS_OK FALSE) endif() @@ -57,23 +90,14 @@ set(LIBMV_LIGHT_LIBS if(Ceres_FOUND) add_definitions("-DCERES_FOUND=1") list(APPEND LIBMV_LIGHT_LIBS simple_pipeline) - list(APPEND LIBMV_LIGHT_INCLUDES "${CERES_INCLUDE_DIR}") + if(Ceres_VERSION VERSION_LESS 2.0.0) + list(APPEND LIBMV_LIGHT_INCLUDES "${CERES_INCLUDE_DIRS}") + endif() else() add_definitions("-DCERES_FOUND=0") message(STATUS "CERES support is disabled. Ceres Solver for reconstruction API is required.") endif() -### COMPILE WITH C++11 IF CERES WAS COMPILED WITH C++11 - -if(Ceres_FOUND) - list (FIND CERES_COMPILED_COMPONENTS "C++11" _index) - if (${_index} GREATER -1) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") - endif() -endif() - -### DEFINE OPENCV SFM MODULE DEPENDENCIES ### - ### CREATE OPENCV SFM MODULE ### ocv_add_module(sfm @@ -85,6 +109,7 @@ ocv_add_module(sfm WRAP python ) +add_definitions(/DGLOG_NO_ABBREVIATED_SEVERITIES) # avoid ERROR macro conflict in glog (ceres dependency) ocv_warnings_disable(CMAKE_CXX_FLAGS -Wundef @@ -97,12 +122,6 @@ ocv_warnings_disable(CMAKE_CXX_FLAGS -Wsuggest-override ) -if(UNIX) - if(CMAKE_COMPILER_IS_GNUCXX OR CV_ICC) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") - endif() -endif() - ocv_include_directories( ${LIBMV_LIGHT_INCLUDES} ) ocv_module_include_directories() @@ -117,14 +136,16 @@ ocv_set_module_sources(HEADERS ${OPENCV_SFM_HDRS} ocv_create_module() -# build libmv_light + +### BUILD libmv_light ### + if(NOT CMAKE_VERSION VERSION_LESS 2.8.11) # See ocv_target_include_directories() implementation if(TARGET ${the_module}) get_target_property(__include_dirs ${the_module} INCLUDE_DIRECTORIES) include_directories(${__include_dirs}) endif() endif() -include_directories(${OCV_TARGET_INCLUDE_DIRS_${the_module}}) +#include_directories(${OCV_TARGET_INCLUDE_DIRS_${the_module}}) add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/src/libmv_light" "${CMAKE_CURRENT_BINARY_DIR}/src/libmv") ocv_target_link_libraries(${the_module} ${LIBMV_LIGHT_LIBS}) @@ -133,6 +154,9 @@ ocv_target_link_libraries(${the_module} ${LIBMV_LIGHT_LIBS}) ### CREATE OPENCV SFM TESTS ### ocv_add_accuracy_tests() +if(Ceres_FOUND AND TARGET opencv_test_sfm) + ocv_target_link_libraries(opencv_test_sfm ${CERES_LIBRARIES}) +endif () ### CREATE OPENCV SFM SAMPLES ### diff --git a/modules/sfm/cmake/checks/check_glog_gflags.cpp b/modules/sfm/cmake/checks/check_glog_gflags.cpp new file mode 100644 index 00000000000..3353419cb3e --- /dev/null +++ b/modules/sfm/cmake/checks/check_glog_gflags.cpp @@ -0,0 +1,7 @@ +#include +#include +int main() +{ + (void)(0); + return 0; +} diff --git a/modules/sfm/src/libmv_light/libmv/base/vector.h b/modules/sfm/src/libmv_light/libmv/base/vector.h index 1931fb0b1f9..ac7a2feead0 100644 --- a/modules/sfm/src/libmv_light/libmv/base/vector.h +++ b/modules/sfm/src/libmv_light/libmv/base/vector.h @@ -121,7 +121,14 @@ class vector { void reserve(unsigned int size) { if (size > size_) { T *data = static_cast(allocate(size)); +#if 0 memcpy(data, data_, sizeof(*data)*size_); +#else + for (int i = 0; i < size_; ++i) + new (&data[i]) T(std::move(data_[i])); + for (int i = 0; i < size_; ++i) + data_[i].~T(); +#endif allocator_.deallocate(data_, capacity_); data_ = data; capacity_ = size; diff --git a/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt b/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt index 5b4b40b95e2..14c77fe5787 100644 --- a/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt +++ b/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt @@ -21,5 +21,8 @@ TARGET_LINK_LIBRARIES(multiview LINK_PRIVATE ${GLOG_LIBRARY} numeric) IF(TARGET Eigen3::Eigen) TARGET_LINK_LIBRARIES(multiview LINK_PUBLIC Eigen3::Eigen) ENDIF() +IF(CERES_LIBRARIES) + TARGET_LINK_LIBRARIES(multiview LINK_PRIVATE ${CERES_LIBRARIES}) +ENDIF() LIBMV_INSTALL_LIB(multiview) diff --git a/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc b/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc index a18dab0ffdf..768c39f5220 100644 --- a/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc +++ b/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc @@ -521,7 +521,7 @@ bool EstimateFundamentalFromCorrespondences( FundamentalSymmetricEpipolarCostFunctor, 2, // num_residuals 9>(fundamental_symmetric_epipolar_cost_function), - NULL, + nullptr, F->data()); } diff --git a/modules/sfm/src/libmv_light/libmv/multiview/homography.cc b/modules/sfm/src/libmv_light/libmv/multiview/homography.cc index 8816ef5aa6e..31d99277112 100644 --- a/modules/sfm/src/libmv_light/libmv/multiview/homography.cc +++ b/modules/sfm/src/libmv_light/libmv/multiview/homography.cc @@ -318,7 +318,7 @@ bool EstimateHomography2DFromCorrespondences( HomographySymmetricGeometricCostFunctor, 4, // num_residuals 9>(homography_symmetric_geometric_cost_function), - NULL, + nullptr, H->data()); } diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc b/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc index 58006e72a2c..1a1568da831 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc @@ -402,7 +402,7 @@ void EuclideanBundlePointsOnly(const DistortionModelType distortion_model, marker.x, marker.y, 1.0)), - NULL, + nullptr, ceres_intrinsics, current_camera_R_t, &point->X(0)); diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc b/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc index d625c111951..f13d35ad66d 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc @@ -113,7 +113,7 @@ bool EuclideanIntersect(const vector &markers, EuclideanIntersectCostFunctor, 2, /* num_residuals */ 3>(new EuclideanIntersectCostFunctor(marker, camera)), - NULL, + nullptr, &X(0)); num_residuals++; } diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc index d5d009708ba..c0d32bebdce 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc @@ -28,10 +28,6 @@ namespace libmv { -Tracks::Tracks(const Tracks &other) { - markers_ = other.markers_; -} - Tracks::Tracks(const vector &markers) : markers_(markers) {} void Tracks::Insert(int image, int track, double x, double y, double weight) { diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h index a54a43659b7..906ec2b6911 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h @@ -65,7 +65,7 @@ class Tracks { Tracks() { } // Copy constructor for a tracks object. - Tracks(const Tracks &other); + Tracks(const Tracks &other) = default; /// Construct a new tracks object using the given markers to start. explicit Tracks(const vector &markers); From 387ad42690e53090eb419d342c805cfe62bafdaa Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Thu, 29 Oct 2020 23:39:55 +0000 Subject: [PATCH 14/29] cmake: support Ceres 2.0.0 backport of commit: 23ee62a19b7a3e50d6dbf295359d8b1aff2e03fd --- modules/sfm/CMakeLists.txt | 90 ++++++++++++------- .../sfm/cmake/checks/check_glog_gflags.cpp | 7 ++ modules/sfm/src/libmv_capi.h | 2 + .../sfm/src/libmv_light/libmv/base/vector.h | 7 ++ .../libmv/multiview/CMakeLists.txt | 3 + .../libmv/multiview/fundamental.cc | 2 +- .../libmv_light/libmv/multiview/homography.cc | 2 +- .../libmv/simple_pipeline/bundle.cc | 2 +- .../libmv/simple_pipeline/intersect.cc | 2 +- .../libmv/simple_pipeline/tracks.cc | 4 - .../libmv/simple_pipeline/tracks.h | 2 +- 11 files changed, 81 insertions(+), 42 deletions(-) create mode 100644 modules/sfm/cmake/checks/check_glog_gflags.cpp diff --git a/modules/sfm/CMakeLists.txt b/modules/sfm/CMakeLists.txt index 53a8d4378a9..045a1fe6e45 100644 --- a/modules/sfm/CMakeLists.txt +++ b/modules/sfm/CMakeLists.txt @@ -3,23 +3,56 @@ set(the_description "SFM algorithms") ### LIBMV LIGHT EXTERNAL DEPENDENCIES ### -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") -find_package(Gflags QUIET) + find_package(Ceres QUIET) -if(NOT Ceres_FOUND) # Looks like Ceres find glog on the own, so separate search isn't necessary + +if(NOT Gflags_FOUND) # Ceres find gflags on the own, so separate search isn't necessary + find_package(Gflags QUIET) +endif() +if(NOT Glog_FOUND) # Ceres find glog on the own, so separate search isn't necessary find_package(Glog QUIET) endif() -if((gflags_FOUND OR GFLAGS_FOUND OR GFLAGS_INCLUDE_DIRS) AND (glog_FOUND OR GLOG_FOUND OR GLOG_INCLUDE_DIRS)) - set(_fname "${CMAKE_CURRENT_BINARY_DIR}/test_sfm_deps.cpp") - file(WRITE "${_fname}" "#include \n#include \nint main() { (void)(0); return 0; }\n") - try_compile(SFM_DEPS_OK "${CMAKE_BINARY_DIR}" "${_fname}" - CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:STRING=${GLOG_INCLUDE_DIRS};${GFLAGS_INCLUDE_DIRS}" - LINK_LIBRARIES ${GLOG_LIBRARIES} ${GFLAGS_LIBRARIES} - OUTPUT_VARIABLE OUTPUT - ) - file(REMOVE "${_fname}") - message(STATUS "Checking SFM deps... ${SFM_DEPS_OK}") +if(NOT Gflags_FOUND OR NOT Glog_FOUND) + # try local search scripts + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + if(NOT Gflags_FOUND) + find_package(Gflags QUIET) + endif() + if(NOT Glog_FOUND) + find_package(Glog QUIET) + endif() +endif() + +if(NOT DEFINED GFLAGS_INCLUDE_DIRS AND DEFINED GFLAGS_INCLUDE_DIR) + set(GFLAGS_INCLUDE_DIRS "${GFLAGS_INCLUDE_DIR}") +endif() +if(NOT DEFINED GLOG_INCLUDE_DIRS AND DEFINED GLOG_INCLUDE_DIR) + set(GLOG_INCLUDE_DIRS "${GLOG_INCLUDE_DIR}") +endif() + +if((gflags_FOUND OR Gflags_FOUND OR GFLAGS_FOUND OR GFLAGS_INCLUDE_DIRS) AND (glog_FOUND OR Glog_FOUND OR GLOG_FOUND OR GLOG_INCLUDE_DIRS)) + set(__cache_key "${GLOG_INCLUDE_DIRS} ~ ${GFLAGS_INCLUDE_DIRS} ~ ${GLOG_LIBRARIES} ~ ${GFLAGS_LIBRARIES}") + if(NOT DEFINED SFM_GLOG_GFLAGS_TEST_CACHE_KEY OR NOT (SFM_GLOG_GFLAGS_TEST_CACHE_KEY STREQUAL __cache_key)) + set(__fname "${CMAKE_CURRENT_LIST_DIR}/cmake/checks/check_glog_gflags.cpp") + try_compile( + SFM_GLOG_GFLAGS_TEST "${CMAKE_BINARY_DIR}" "${__fname}" + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:STRING=${GLOG_INCLUDE_DIRS};${GFLAGS_INCLUDE_DIRS}" + LINK_LIBRARIES ${GLOG_LIBRARIES} ${GFLAGS_LIBRARIES} + OUTPUT_VARIABLE __output + ) + if(NOT SFM_GLOG_GFLAGS_TEST) + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log + "Failed compilation check: ${__fname}\n" + "${__output}\n\n" + ) + endif() + set(SFM_GLOG_GFLAGS_TEST "${SFM_GLOG_GFLAGS_TEST}" CACHE INTERNAL "") + set(SFM_GLOG_GFLAGS_TEST_CACHE_KEY "${__cache_key}" CACHE INTERNAL "") + message(STATUS "Checking SFM glog/gflags deps... ${SFM_GLOG_GFLAGS_TEST}") + endif() + unset(__cache_key) + set(SFM_DEPS_OK "${SFM_GLOG_GFLAGS_TEST}") else() set(SFM_DEPS_OK FALSE) endif() @@ -57,23 +90,14 @@ set(LIBMV_LIGHT_LIBS if(Ceres_FOUND) add_definitions("-DCERES_FOUND=1") list(APPEND LIBMV_LIGHT_LIBS simple_pipeline) - list(APPEND LIBMV_LIGHT_INCLUDES "${CERES_INCLUDE_DIR}") + if(Ceres_VERSION VERSION_LESS 2.0.0) + list(APPEND LIBMV_LIGHT_INCLUDES "${CERES_INCLUDE_DIRS}") + endif() else() add_definitions("-DCERES_FOUND=0") message(STATUS "CERES support is disabled. Ceres Solver for reconstruction API is required.") endif() -### COMPILE WITH C++11 IF CERES WAS COMPILED WITH C++11 - -if(Ceres_FOUND) - list (FIND CERES_COMPILED_COMPONENTS "C++11" _index) - if (${_index} GREATER -1) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") - endif() -endif() - -### DEFINE OPENCV SFM MODULE DEPENDENCIES ### - ### CREATE OPENCV SFM MODULE ### ocv_add_module(sfm @@ -85,6 +109,7 @@ ocv_add_module(sfm WRAP python ) +add_definitions(/DGLOG_NO_ABBREVIATED_SEVERITIES) # avoid ERROR macro conflict in glog (ceres dependency) ocv_warnings_disable(CMAKE_CXX_FLAGS -Wundef @@ -97,12 +122,6 @@ ocv_warnings_disable(CMAKE_CXX_FLAGS -Wsuggest-override ) -if(UNIX) - if(CMAKE_COMPILER_IS_GNUCXX OR CV_ICC) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") - endif() -endif() - ocv_include_directories( ${LIBMV_LIGHT_INCLUDES} ) ocv_module_include_directories() @@ -117,14 +136,16 @@ ocv_set_module_sources(HEADERS ${OPENCV_SFM_HDRS} ocv_create_module() -# build libmv_light + +### BUILD libmv_light ### + if(NOT CMAKE_VERSION VERSION_LESS 2.8.11) # See ocv_target_include_directories() implementation if(TARGET ${the_module}) get_target_property(__include_dirs ${the_module} INCLUDE_DIRECTORIES) include_directories(${__include_dirs}) endif() endif() -include_directories(${OCV_TARGET_INCLUDE_DIRS_${the_module}}) +#include_directories(${OCV_TARGET_INCLUDE_DIRS_${the_module}}) add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/src/libmv_light" "${CMAKE_CURRENT_BINARY_DIR}/src/libmv") ocv_target_link_libraries(${the_module} ${LIBMV_LIGHT_LIBS}) @@ -133,6 +154,9 @@ ocv_target_link_libraries(${the_module} ${LIBMV_LIGHT_LIBS}) ### CREATE OPENCV SFM TESTS ### ocv_add_accuracy_tests() +if(Ceres_FOUND AND TARGET opencv_test_sfm) + ocv_target_link_libraries(opencv_test_sfm ${CERES_LIBRARIES}) +endif () ### CREATE OPENCV SFM SAMPLES ### diff --git a/modules/sfm/cmake/checks/check_glog_gflags.cpp b/modules/sfm/cmake/checks/check_glog_gflags.cpp new file mode 100644 index 00000000000..3353419cb3e --- /dev/null +++ b/modules/sfm/cmake/checks/check_glog_gflags.cpp @@ -0,0 +1,7 @@ +#include +#include +int main() +{ + (void)(0); + return 0; +} diff --git a/modules/sfm/src/libmv_capi.h b/modules/sfm/src/libmv_capi.h index 3c39c2b5c71..057bf4b566d 100644 --- a/modules/sfm/src/libmv_capi.h +++ b/modules/sfm/src/libmv_capi.h @@ -42,6 +42,8 @@ #ifndef __OPENCV_SFM_LIBMV_CAPI__ #define __OPENCV_SFM_LIBMV_CAPI__ +#include + #include "libmv/logging/logging.h" #include "libmv/correspondence/feature.h" diff --git a/modules/sfm/src/libmv_light/libmv/base/vector.h b/modules/sfm/src/libmv_light/libmv/base/vector.h index 1931fb0b1f9..9740cfaf6fc 100644 --- a/modules/sfm/src/libmv_light/libmv/base/vector.h +++ b/modules/sfm/src/libmv_light/libmv/base/vector.h @@ -121,7 +121,14 @@ class vector { void reserve(unsigned int size) { if (size > size_) { T *data = static_cast(allocate(size)); +#if defined(__GNUC__) && __GNUC__ < 5 // legacy compilers branch memcpy(data, data_, sizeof(*data)*size_); +#else + for (int i = 0; i < size_; ++i) + new (&data[i]) T(std::move(data_[i])); + for (int i = 0; i < size_; ++i) + data_[i].~T(); +#endif allocator_.deallocate(data_, capacity_); data_ = data; capacity_ = size; diff --git a/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt b/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt index 5b4b40b95e2..14c77fe5787 100644 --- a/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt +++ b/modules/sfm/src/libmv_light/libmv/multiview/CMakeLists.txt @@ -21,5 +21,8 @@ TARGET_LINK_LIBRARIES(multiview LINK_PRIVATE ${GLOG_LIBRARY} numeric) IF(TARGET Eigen3::Eigen) TARGET_LINK_LIBRARIES(multiview LINK_PUBLIC Eigen3::Eigen) ENDIF() +IF(CERES_LIBRARIES) + TARGET_LINK_LIBRARIES(multiview LINK_PRIVATE ${CERES_LIBRARIES}) +ENDIF() LIBMV_INSTALL_LIB(multiview) diff --git a/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc b/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc index a18dab0ffdf..768c39f5220 100644 --- a/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc +++ b/modules/sfm/src/libmv_light/libmv/multiview/fundamental.cc @@ -521,7 +521,7 @@ bool EstimateFundamentalFromCorrespondences( FundamentalSymmetricEpipolarCostFunctor, 2, // num_residuals 9>(fundamental_symmetric_epipolar_cost_function), - NULL, + nullptr, F->data()); } diff --git a/modules/sfm/src/libmv_light/libmv/multiview/homography.cc b/modules/sfm/src/libmv_light/libmv/multiview/homography.cc index 8816ef5aa6e..31d99277112 100644 --- a/modules/sfm/src/libmv_light/libmv/multiview/homography.cc +++ b/modules/sfm/src/libmv_light/libmv/multiview/homography.cc @@ -318,7 +318,7 @@ bool EstimateHomography2DFromCorrespondences( HomographySymmetricGeometricCostFunctor, 4, // num_residuals 9>(homography_symmetric_geometric_cost_function), - NULL, + nullptr, H->data()); } diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc b/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc index 58006e72a2c..1a1568da831 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/bundle.cc @@ -402,7 +402,7 @@ void EuclideanBundlePointsOnly(const DistortionModelType distortion_model, marker.x, marker.y, 1.0)), - NULL, + nullptr, ceres_intrinsics, current_camera_R_t, &point->X(0)); diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc b/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc index d625c111951..f13d35ad66d 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/intersect.cc @@ -113,7 +113,7 @@ bool EuclideanIntersect(const vector &markers, EuclideanIntersectCostFunctor, 2, /* num_residuals */ 3>(new EuclideanIntersectCostFunctor(marker, camera)), - NULL, + nullptr, &X(0)); num_residuals++; } diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc index d5d009708ba..c0d32bebdce 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.cc @@ -28,10 +28,6 @@ namespace libmv { -Tracks::Tracks(const Tracks &other) { - markers_ = other.markers_; -} - Tracks::Tracks(const vector &markers) : markers_(markers) {} void Tracks::Insert(int image, int track, double x, double y, double weight) { diff --git a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h index a54a43659b7..906ec2b6911 100644 --- a/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h +++ b/modules/sfm/src/libmv_light/libmv/simple_pipeline/tracks.h @@ -65,7 +65,7 @@ class Tracks { Tracks() { } // Copy constructor for a tracks object. - Tracks(const Tracks &other); + Tracks(const Tracks &other) = default; /// Construct a new tracks object using the given markers to start. explicit Tracks(const vector &markers); From ad756614be4da6f57c84870b3bc31ed77ec4dd8d Mon Sep 17 00:00:00 2001 From: EricFlorin Date: Sat, 7 Nov 2020 07:46:00 -0800 Subject: [PATCH 15/29] Merge pull request #2610 from EricFlorin:VizWindowNamePrepend_Contrib * Edited Documentation Edited the Viz "getWindowByName" documentation so it reflects the actual behaviour of getWindowByName. More specifically, that "Viz - " is not prefixed automatically to the window name given by the user if they haven't prefixed "Viz - " already. --- modules/viz/include/opencv2/viz/vizcore.hpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/viz/include/opencv2/viz/vizcore.hpp b/modules/viz/include/opencv2/viz/vizcore.hpp index 85b75131d68..7579ddc3ee7 100644 --- a/modules/viz/include/opencv2/viz/vizcore.hpp +++ b/modules/viz/include/opencv2/viz/vizcore.hpp @@ -92,12 +92,7 @@ namespace cv @note If the window with that name already exists, that window is returned. Otherwise, new window is created with the given name, and it is returned. - @note Window names are automatically prefixed by "Viz - " if it is not done by the user. - @code - /// window and window_2 are the same windows. - viz::Viz3d window = viz::getWindowByName("myWindow"); - viz::Viz3d window_2 = viz::getWindowByName("Viz - myWindow"); - @endcode + */ CV_EXPORTS Viz3d getWindowByName(const String &window_name); From ddafc73c53053a86ddf9b4ed3f13d2d74618ddc7 Mon Sep 17 00:00:00 2001 From: sunitanyk Date: Fri, 13 Nov 2020 13:40:03 -0800 Subject: [PATCH 16/29] Merge pull request #2729 from sunitanyk:master Added Python wrapping and documentation for alphamat module * updated documentation with more results for alphamat module * Updated the image links in the tutorial * Included cite --- modules/alphamat/CMakeLists.txt | 1 + modules/alphamat/README.md | 4 +-- modules/alphamat/include/opencv2/alphamat.hpp | 19 +++++++--- .../samples/information_flow_matting.cpp | 12 ++++++- .../samples/output_mattes/plant_result.jpg | Bin 38338 -> 0 bytes .../samples/output_mattes/plant_result.png | Bin 0 -> 38449 bytes .../tutorials/alphamat_tutorial.markdown | 34 +++++++++++------- .../alphamat/tutorials/matting_results.jpg | Bin 0 -> 269776 bytes .../tutorials/plant_new_backgrounds.jpg | Bin 0 -> 144052 bytes 9 files changed, 50 insertions(+), 20 deletions(-) delete mode 100644 modules/alphamat/samples/output_mattes/plant_result.jpg create mode 100644 modules/alphamat/samples/output_mattes/plant_result.png create mode 100644 modules/alphamat/tutorials/matting_results.jpg create mode 100644 modules/alphamat/tutorials/plant_new_backgrounds.jpg diff --git a/modules/alphamat/CMakeLists.txt b/modules/alphamat/CMakeLists.txt index f5c9d8917f2..32fca08a15d 100644 --- a/modules/alphamat/CMakeLists.txt +++ b/modules/alphamat/CMakeLists.txt @@ -6,4 +6,5 @@ endif() ocv_define_module(alphamat opencv_core opencv_imgproc + WRAP python ) diff --git a/modules/alphamat/README.md b/modules/alphamat/README.md index e3dbe6bf443..abddf9b601f 100644 --- a/modules/alphamat/README.md +++ b/modules/alphamat/README.md @@ -7,12 +7,12 @@ This project was part of the Google Summer of Code 2019. *** Alphamatting is the problem of extracting the foreground from an image. Given the input of an image and its corresponding trimap, we try to extract the foreground from the background. -This project is implementation of "[[Designing Effective Inter-Pixel Information Flow for Natural Image Matting](http://people.inf.ethz.ch/aksoyy/ifm/)]" by Yağız Aksoy, Tunç Ozan Aydın and Marc Pollefeys[1]. It required implementation of parts of other papers [2,3,4]. +This project is implementation of "[Designing Effective Inter-Pixel Information Flow for Natural Image Matting](https://www.researchgate.net/publication/318489370_Designing_Effective_Inter-Pixel_Information_Flow_for_Natural_Image_Matting)" by Yağız Aksoy, Tunç Ozan Aydın and Marc Pollefeys[1]. It required implementation of parts of other papers [2,3,4]. ## References -[1] Yagiz Aksoy, Tunc Ozan Aydin, Marc Pollefeys, "[Designing Effective Inter-Pixel Information Flow for Natural Image Matting](http://people.inf.ethz.ch/aksoyy/ifm/)", CVPR, 2017. +[1] Yagiz Aksoy, Tunc Ozan Aydin, Marc Pollefeys, "[Designing Effective Inter-Pixel Information Flow for Natural Image Matting](https://www.researchgate.net/publication/318489370_Designing_Effective_Inter-Pixel_Information_Flow_for_Natural_Image_Matting)", CVPR, 2017. [2] Roweis, Sam T., and Lawrence K. Saul. "[Nonlinear dimensionality reduction by locally linear embedding](https://science.sciencemag.org/content/290/5500/2323)" Science 290.5500 (2000): 2323-2326. diff --git a/modules/alphamat/include/opencv2/alphamat.hpp b/modules/alphamat/include/opencv2/alphamat.hpp index 927fbb09eac..003b305e5e6 100644 --- a/modules/alphamat/include/opencv2/alphamat.hpp +++ b/modules/alphamat/include/opencv2/alphamat.hpp @@ -9,8 +9,15 @@ /** * @defgroup alphamat Alpha Matting - * This module is dedicated to compute alpha matting of images, given the input image and an input trimap. - * The samples directory includes easy examples of how to use the module. + * Alpha matting is used to extract a foreground object with soft boundaries from a background image. + * + * This module is dedicated to computing alpha matte of objects in images from a given input image and a greyscale trimap image that contains information about the foreground, background and unknown pixels. The unknown pixels are assumed to be a combination of foreground and background pixels. The algorithm uses a combination of multiple carefully defined pixels affinities to estimate the opacity of the foreground pixels in the unkown region. + * + * The implementation is based on @cite aksoy2017designing. + * + * This module was developed by Muskaan Kularia and Sunita Nayak as a project + * for Google Summer of Code 2019 (GSoC 19). + * */ namespace cv { namespace alphamat { @@ -18,10 +25,12 @@ namespace cv { namespace alphamat { //! @{ /** - * The implementation is based on Designing Effective Inter-Pixel Information Flow for Natural Image Matting by Yağız Aksoy, Tunç Ozan Aydın and Marc Pollefeys, CVPR 2019. + * @brief Compute alpha matte of an object in an image + * @param image Input RGB image + * @param tmap Input greyscale trimap image + * @param result Output alpha matte image * - * This module has been originally developed by Muskaan Kularia and Sunita Nayak as a project - * for Google Summer of Code 2019 (GSoC 19). + * The function infoFlow performs alpha matting on a RGB image using a greyscale trimap image, and outputs a greyscale alpha matte image. The output alpha matte can be used to softly extract the foreground object from a background image. Examples can be found in the samples directory. * */ CV_EXPORTS_W void infoFlow(InputArray image, InputArray tmap, OutputArray result); diff --git a/modules/alphamat/samples/information_flow_matting.cpp b/modules/alphamat/samples/information_flow_matting.cpp index f4dbda1d002..679111ea03d 100644 --- a/modules/alphamat/samples/information_flow_matting.cpp +++ b/modules/alphamat/samples/information_flow_matting.cpp @@ -2,16 +2,19 @@ // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html. +// Include relevant headers #include #include "opencv2/highgui.hpp" #include #include #include +// Set namespaces using namespace std; using namespace cv; using namespace cv::alphamat; +// Set the usage parameter names const char* keys = "{img || input image name}" "{tri || input trimap image name}" @@ -30,10 +33,12 @@ int main(int argc, char* argv[]) return 0; } + // Read the paths to the input image, input trimap and the location of the output image. string img_path = parser.get("img"); string trimap_path = parser.get("tri"); string result_path = parser.get("out"); + // Make sure the user inputs paths to the input image and trimap if (!parser.check() || img_path.empty() || trimap_path.empty()) { @@ -44,13 +49,15 @@ int main(int argc, char* argv[]) Mat image, tmap; - image = imread(img_path, IMREAD_COLOR); // Read the input image file + // Read the input image + image = imread(img_path, IMREAD_COLOR); if (image.empty()) { printf("Cannot read image file: '%s'\n", img_path.c_str()); return 1; } + // Read the trimap tmap = imread(trimap_path, IMREAD_GRAYSCALE); if (tmap.empty()) { @@ -59,16 +66,19 @@ int main(int argc, char* argv[]) } Mat result; + // Perform information flow alpha matting infoFlow(image, tmap, result); if (result_path.empty()) { + // Show the alpha matte if a result filepath is not provided. namedWindow("result alpha matte", WINDOW_NORMAL); imshow("result alpha matte", result); waitKey(0); } else { + // Save the alphamatte imwrite(result_path, result); printf("Result saved: '%s'\n", result_path.c_str()); } diff --git a/modules/alphamat/samples/output_mattes/plant_result.jpg b/modules/alphamat/samples/output_mattes/plant_result.jpg deleted file mode 100644 index 4ec7e29c6b049aa870da9e00ffa3fa90cfdd6ce8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38338 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�r7fdEGqFcUUkMg43 z>5*x*V<1->*TU`GwaL+Xy_ac5rh|GJN>JEE1;|@46WP*A;$7Md$SiFRfQ$VgnPFtI zY#&+j_6+n5d0Kd4>2& zFWI_XZ?{po*wy|*=|Kp}$u4WB`!j z+^ksa79!9Ngj;w!-oSO|IQuc(v`b%qa4Sc4NotZ}2=e1!Yb-wy>NTiQ-lUSSg#S3( z9Am|~j1Y`}wRav}Uo5xk|CDfaP`1$nB5T6>w!u^onNMKP(n~NG7@H4EzoWY3F~|O5 z0n?hQ$b?_ZHad>EKx2o`MRjBcu*2UWGu*l;hoL?Kk&XJ`3H_AT74bZWnk3DDAu=S}}5l7NZxAj75O9t;0RSj0fw7^HW~YlAWICAEzgSM-*PxXS8u0gH6eUpa%cXxa2_}44i$% z9@Z}(AbDriW-xet?KXT}VlS<8t~}c*VSM=hR{_C|T7tMZiS=VlIMNgcA6&o8C}hAw zh!~K!eVHk8?W5$I6vE{-IHR|VF+o*^eBI##$%F6}*5d8&Es&c{Z57qB>yEc$+U96SZIN8$v z;bPEvWbkT`$UGgM_QwqG2*jHNhzWt4=t2@EoZ^%RS@vnMiyJd~&GkhaHw);ms#Lx} zR2I0*6#7L3&rwSa>*={Wy<^-PKqCj?S4ftGuyWvtSfEYsv#0GwA;o1Z{ zN&?b;SBZLjKwU)B!Y91B51M7p1hH2nn&RMLznGOEC36Vqu&5?*>@&$EN!rMs-l8~{ z!BYA&Vv-MU%6h$UYF+<^JAE8Vt~h%SDGR<1Xc@e)x`qTCC*1Nb#;K#}LgIGgHp-iN(hO7hqzR(y`biLx| zX)1S9(`nQBNXO)@;Tcz4VSgrr85Qy=3<4>Y z?qyL|{2wp^1dvBpO(hq5TtO=ygQd+fL$omJ-NKI}W(hH6Prhs92`8=Fu&34H+V7-Q ztrzq8Sxa89lY3!U<4&0*HGMdSk&U3VWRh&!0!WXaolxMO>hlH_{W1(b<+y19FMWMJ zIej#$;X$UA0p=VfMDwCZgRurUXsZsM($fqH3gTTR3}9^K$Nb7iS8S>e+^El&JcD^2 zvZW44%7)&B4>K-%pg<`-zKn>0J{VQ#K6f)$*th4xRW@SZzkp4xPw$Z3Chq*=xKC#! z_R~Ip%=@n-nKqSZ6&bJzpkOw2n>h`}PkIq(#w0br);Nf!DC;&ghrVuRxY&tY3F`wIW7t37s1L)i@q-(T?J`%8eJYB6vP@r0w${BLE0JD|Y#Tn}W4H_~ z)!Jb=H-Tu|uvnIP#!+IpvKZC!!tSHeIoA%~$u$-3+9kjc|3(0b&m1kY4B$&eoXo)r zu7A~HQ4}zJep8w(ThTO6bEkMla@k1g);3pq!^`~MUU!|+TUW{H5cZgxm8e6>Cpq0d zZ`HWLTYVEkE+Z~855C)5%>4^0{`-1lD$y>U>$43f+zLohG_l4T;*hDK5+$@q8Uozt zyTWfHC5_aNdsfPF?cCzrr<@{uzSD9(fARr{n*#y)paTe!ts^lvmZRd#xNX+ImT$$j z&3(P&5DZID7@-K=QN?r$5^kK6I64PG^}scy_y-s7G(aI3WP-J|{ER>;ht|>u`$_#f z(xRf21=1bs-8Pgg+}!#{lH}_WLQfFbLv^8@qJYnR0^krxcT>!4qFd+t# zXJHIK=MKZn7y*kStk~9?8Z3jySwXEdSvXkxyG!U=Yj_{2IalevY?VTZ&kgpPc#|cC zT6Ra!3z82;@{rXsEm#Vo_YpbcPpq@#E?KL(ijsN7z<_QGqz@@&BCC<)a5ZI7L`jnE zMTLf<7pAkqN0O1vYpl|KU+MG;^5@3#6n7Txp{u-b68AlW?DB1M0wsV<&4CR`6S|1R znj!_jgwv7dbQ_|Cn5uQNkdFXBI((65e0u?Ucr&ukPRVHw?RqWob%7>21k8Z6k;>x| zPqorLIdaoLolUEqsBG45C4RCiXLXlN)OZnX`TYOm;i|#cBF(IE1+CP{P4r6JfANR? zb~ek1uG8_I7pXj#r+;GEg3T(;i4{Z^k`Pd@M8O3q6p-nq&8>s9lG4HG>3R>eTT|Mx zMRX3IuK0vl>O%}y(k5?LUc*2a&mTaoXAA@nCg>~xNV8r?VM|fc)yM(HOZJ)bdLQ?u zi-xO`REZ{Qa*Vfcj#JW!=EZk-WFpi^3yWdzL)k4X!oMUuj^m`Hh^A1X2!fA7^8i{z z@M|TYuW<&$fuf0BW5Cr;^!g1!T8MPPPa|$J@>|Z0w5R8FHj@^HjoIwUB;#TZ1UO*l znOWGC$&eArj8ge%^xM?{b+~%!gez`1swSNFytUIO;pg4k12sB-mu3Ny@UR|0+!f&D zzks<(XY37vGJ3cWHKp|Uu6x14L*v9WeRqpt{ivVr{)RGZs?<9d7C#-p#&U)Z4>Ox5 zooVSVFz>Qyj>rBMEfNB1K}lVf0$&<#AGW@x@lwiA@a=)ylDB}6&8dr{#9I-=Bm{T!x8VS*y#?!D$ zXxhlEZ0r5i`}BD|@8|jN_t_u6``-8dUia_1ulu^LuRAyTx<6WVxw^f}_^WEH3uVE? zf~g~$@qxS>K21cD>Py#Q{CM`-r^&~`CP6;i$Arqf$AId6K#*{1&FhK-MoP@r=|KOL z$sC~jk(2=sxNQY##}&!h31u`m-z>H^jYjvNvH18t42vTRiUR?7EFXs0`WJc^W1phTnWIG7lgyuYNxDK z{P?6$dfN;dNhO1Y$MAIqi|tbFTC}(WSvN)j4!PW+p1iDWN#0*wS-Gww+Dr>w(T zW8H&vC=<+*nSF-w=Y&w3i%3{=E&I<)8L4u0HCaj9q9nH#d(hsMRXN5@rcahXku!aD z;79QDn4ezexdN5{y^l+~x`QZAvSi5gcH7z4;PE@T6p(`QTi%&WqAJ2*S@3P1 zre|D&K8Pmlpd+MSgjsaM7QX@Am2gl1R3O;k|6Sxdwt8QCwpb8c1G6Li8PA2XB`9(C z(5hNAV&OnT-i$BuOFRyea{M<}%*`{eHP6R_TW&FZk6J!ZRaGxy%#9WnY}i*bhE-z* z7f;<1Pwfkx&Uo*E1J5A)sQevFAbXje&n#uRqaxX*oyQQV?agw6JFHLc&}esNX94NW zB;hmN&0LugB!a&$7UTmrA_qDFpzdRxxSbVduJ{sJsUlBI|b8-}9`xr&oO){$C ze25d5Ai$ZyU>Gnnq%xecGDQWYS*tA^l=wt4S6ormrzBT2>Qwxd{Pnh<(aY>f;=~#^ zlXqm@iLX3N4gyaQJ2?x9$|I^(4@;*c5dB(g5ubqtBl{cvu+1l%@A&H?@G1bn^;n9r z6+m4;fXrqe6+iL3qn^u9va$%o)rJYJISc5~O+W)JGpU1ox!&9 z;wuoQ$M_19(|v*P269qeI2aQ=gofrG2i%$c=h4V>frRXrZ(;;{hb0kYGkMojwv4`z zcya0TTjQS~aRJ6(E3_{8$zIxDC8btl5`k4l|DOx?(-%t!|B=fPGsx0jM`H|mDmzlN zM$U8K+0o@NN0n3=c@M!e-RFN8Cesal8v{TVHW8v$?muq*5(T9g__`aww&Sh(m_W9d zr$aF^t{Jfp>^+`6H5K>`vo)vy^V0U}Q0RLRmuK0F7v5b);3eCMvY z;7k3okIa$=j-?TvUoP9)PSyJ`*`)Ff1@y);=Bo?tGe-=C@;v+KJEL;X7o_A?JKDW; z{A_OQ_KQh^IR5s;1Z>e}_Hk$A>!0IsKt**aG@_Zb#|6k}G>cdIyn-$T3pj7cd-u`pb6 zo*)0~7Kc<5@BN2nR+Dv$af+4T2kvgOaRalb4A!k@HouqN7AOkt&%J46DLTIL0A)@L z5CEZo>0&@nf;^4KD*2dc0?-$z8VOLJ~!z(Ct?vmR~!@b8r$;J#m+?pDe$p)exh`F_^a$ z;^k}Vhp)ognBQDo&)6hr@VBk~ z{$>?Fxvh;JQ2tnET$dIA4jt_?3PqeeqTiOa31r^WZKvrzPOd?A0?8Eqc;4Q*cKp^| z2O{z5Ugug^i&~xO=x2>JP1w68*OrMosBXIfQMZol#Z$4Uy+#RFE;{@=i~KNz6OQH@ zkFjRJHDLT0>q7Y=qyBS8_5{BDt>zUvPpmB`SoxY!agb&!ZBBx4vu1T2R#j_+?hez|qcHsTNyUum}y zmw%oiE$oh2WQK4vZ{-v(pS^;}cN~qr=jEU>e%MgO#6*3Q=6la1@hwN*DSuvdqBFYG z4~=MZc62dhJa|Mm(oNWtXHWZeE*67!B3}A=+Rkp)ai113k~CO9F1QL0I(3NKDaa5i z3E$8I?m=>RW0X;^+oMK$5YGrTT_ z%t8R$(rI2Fr)GHJ?Po`8l{BTEo^QewC)wr|W;DLH!z(pK#xb>*7jJ$$u(N&V} zKbzriuJ0YePs^L}*R57E`TmaD8OWpu;DmycmpMaN00k=VUPo!3si>L*2*bgr76 z{Fg`Zi}m%)Y=itb85MBOcqux(ivSKo;uM+W)IT^t!VEDyT=H|Y$JKl+qvzL%PC5DHZt+rJUc|9IZ(nGLma|B?yBhMB_GVt~T02Ym7>*O9j5 zn-NlE@`9sxE8wnc<|i}6eFW|X{Q4-D5$m#ki(w%kflPn$|0l#_EgDDczYt#{c~&yq zocw7ZAR@oeH$9MlIkW+*z)wIu@j>+M)xB)_3dZkO1OynW8tO8$59?8#4Ziy3>7A9w zG{TNPm?dlguooX8ZImGbbXY(;brxi zoxi-O$vSf^tiO`)QKrs=0CJfkMH+H92c)wKzw=Mi58%aFv`Zz>EQ6m>he2|X%3vCj zeHtv5*kA8e;Sz-1Y^`BPt1s8{Qgd)AEWv9=>(S|*(<>=!MCxuv#M#|A5Oj>MZ~0#{ z-7sq+Ds>i6uIKMx4-UbreJKq4F5);A{ar*U*z)hHr#WJ={uk9cb1Ql#wEJz9KstWz z+!c1i?15BH?9AJAj$QlcSmeCV&f&^cwLOfZ+BpvK1W)a#;+!To5C|Xqal_!bntnwh z3n9D=0H8|Ya4Hul>A+?20|gIRtDUq8t0PR-k!XWPMpWTQ_te=b5L5xmf_*1EH0@M1XXD+|E*Wsm}AbIIqZ)>p;MFw;&qeE)*%0w=NTKx+(o1X;A@FSaoW>>VH?e-qw|ltEulM&jsnxcX z{3p~Ke$*dLFS92E8(>v~d3LsLOI67#oDS6lCh?>I_>^5e`CTN)ARDK_c0(J?bd9|1 zdct>McYPO0t$G)B^7kA2&^=!fx+SFLzm_@17EI8$W_N%a_Fp)a)5z z@0;FVFWJDu|3{^kNug9BC&UX-v&3Ljd6eP_YK(Of=isrX11_qDuL8K{Kn=Xw^IKDr z6n)3TJw(=+BaM&~S8=}9gD|mE6p>8tF_MH&@{PS<92PW5KcM_O0XS|AYD2nj%8_}|=c|32|Q DLy22# diff --git a/modules/alphamat/samples/output_mattes/plant_result.png b/modules/alphamat/samples/output_mattes/plant_result.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a2e93f1ddc814be93adb1b53800e26beec67e2 GIT binary patch literal 38449 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 S>%jRg5a=%p8}o8JDdztIyv8^H literal 0 HcmV?d00001 diff --git a/modules/alphamat/tutorials/alphamat_tutorial.markdown b/modules/alphamat/tutorials/alphamat_tutorial.markdown index 03cd329f5f2..db9f6f72fad 100644 --- a/modules/alphamat/tutorials/alphamat_tutorial.markdown +++ b/modules/alphamat/tutorials/alphamat_tutorial.markdown @@ -7,15 +7,26 @@ This project was part of Google Summer of Code 2019. *Mentor:* Sunita Nayak -Alphamatting is the problem of extracting the foreground from an image. The extracted foreground can be used for further operations like changing the background in an image. +Alphamatting is the problem of extracting the foreground with soft boundaries from a background image. The extracted foreground can be used for further operations like changing the background in an image. Given an input image and its corresponding trimap, we try to extract the foreground from the background. Following is an example: -Input Image: ![](samples/input_images/plant.jpg) -Input Trimap: ![](samples/trimaps/plant.png) -Output alpha Matte: ![](samples/output_mattes/plant_result.jpg) +Input Image: ![](alphamat/samples/input_images/plant.jpg) +Input image should be preferably a RGB image. -This project is implementation of @cite aksoy2017designing . It required implementation of parts of other papers [2,3,4]. +Input Trimap: ![](alphamat/samples/trimaps/plant.png) +The trimap image is a greyscale image that contains information about the foreground(white pixels), background(black pixels) and unknown(grey) pixels. + +Output alpha Matte: ![](alphamat/samples/output_mattes/plant_result.png) +The computed alpha matte is saved as a greyscale image where the pixel values indicate the opacity of the extracted foreground object. These opacity values can be used to blend the foreground object into a diffferent backgound, as shown below: +![](plant_new_backgrounds.jpg) + +Following are some more results. +![](matting_results.jpg) + +The first column is input RGB image, the second column is input trimap, third column is the extracted alpha matte and the last two columns show the foreground object blended on new backgrounds. + +This project is implementation of @cite aksoy2017designing . It also required implementation of parts of other papers [2,3,4]. # Building @@ -33,21 +44,20 @@ Please refer to OpenCV building tutorials for further details, if needed. The built target can be tested as follows: ``` -example_alphamat_information_flow_matting -img= -tri= -out= +/bin/example_alphamat_information_flow_matting -img= -tri= -out= ``` # Source Code of the sample @includelineno alphamat/samples/information_flow_matting.cpp - # References -[1] Yagiz Aksoy, Tunc Ozan Aydin, Marc Pollefeys, "[Designing Effective Inter-Pixel Information Flow for Natural Image Matting](http://people.inf.ethz.ch/aksoyy/ifm/)", CVPR, 2017. +[1] Yagiz Aksoy, Tunc Ozan Aydin, Marc Pollefeys, [Designing Effective Inter-Pixel Information Flow for Natural Image Matting](https://www.researchgate.net/publication/318489370_Designing_Effective_Inter-Pixel_Information_Flow_for_Natural_Image_Matting), CVPR, 2017. -[2] Roweis, Sam T., and Lawrence K. Saul. "[Nonlinear dimensionality reduction by locally linear embedding](https://science.sciencemag.org/content/290/5500/2323)" Science 290.5500 (2000): 2323-2326. +[2] Roweis, Sam T., and Lawrence K. Saul. [Nonlinear dimensionality reduction by locally linear embedding](https://science.sciencemag.org/content/290/5500/2323), Science 290.5500 (2000): 2323-2326. -[3] Anat Levin, Dani Lischinski, Yair Weiss, "[A Closed Form Solution to Natural Image Matting](https://www.researchgate.net/publication/5764820_A_Closed-Form_Solution_to_Natural_Image_Matting)", IEEE TPAMI, 2008. +[3] Anat Levin, Dani Lischinski, Yair Weiss, [A Closed Form Solution to Natural Image Matting](https://www.researchgate.net/publication/5764820_A_Closed-Form_Solution_to_Natural_Image_Matting), IEEE TPAMI, 2008. -[4] Qifeng Chen, Dingzeyu Li, Chi-Keung Tang, "[KNN Matting](http://dingzeyu.li/files/knn-matting-tpami.pdf)", IEEE TPAMI, 2013. +[4] Qifeng Chen, Dingzeyu Li, Chi-Keung Tang, [KNN Matting](http://dingzeyu.li/files/knn-matting-tpami.pdf), IEEE TPAMI, 2013. -[5] Yagiz Aksoy, "[Affinity Based Matting Toolbox](https://github.com/yaksoy/AffinityBasedMattingToolbox)". +[5] Yagiz Aksoy, [Affinity Based Matting Toolbox](https://github.com/yaksoy/AffinityBasedMattingToolbox). diff --git a/modules/alphamat/tutorials/matting_results.jpg b/modules/alphamat/tutorials/matting_results.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebcf5b1d86d7e4bfba1dca699086ada70cb1ecb9 GIT binary patch literal 269776 zcmbTdcT`hPy9OGHAV?9A-V_j!A|ky;RJw>Fy#+)>KzeV9NRuvIsUjeqi1Z?nE+Eo- zP3R?%P(mPuo8Nc7bI!Wwoq7`@iZ&WTOA>Lk-;&m#VtH;)E#P(Iet1gZRl$d~ovcP+1NQaxo!)Kh~5#Cm6KOc zR8oHQSW`<|M_2EKv5BdfxrL>}E63MPZ=79xeEs|b0)v7hBBP>XKE%eQrln_OW_|jc z{k5pLi6j>}B_#zV^*?fvkq7)EI1}ZSThdg_8qcZiy;ub9 zhhM$%Fr~1zlSWX+2*vu+dyJM%NEUt@{f}t>lI;H`*!%xqlKq!p|1H-7;1OwI{%0X4 zRmdsGNri%v;-5nKKgyMV6{>#<^?#L1(jfng@Xu+aFG>0<|JC+?u3RpWmL>3V9zaJy zMp{f1OaLH&7^u$Q**Li9e^q><1`d4Xko2t?N^XU0h*x_^doom}J;y)amcKQsrOc7Z zwz^{(6Dk=BTMc{+EW_)9B#aXz;8kJIB z|Mt}?wmP|Dt#KiQ?ice9PlF%`5)dBBM2V|yde%VQw49>p7kqoOc)ld6{Y?0A0C)IS z0@sC8H|**PLO#QD#i_3OM?@`wEYs6?;zU$w+MBX<<*{0;@pB)QKe&p)&+_M%@Tz3rKn*VtT&?02vwbx`@(Mhn1 zyYXMokGQ$zuN3EL0DpqP#v&?a&6{;{P?|j=eKJl;fAZ~>ilNOr@|OVRCWFD%_l&G3 zin)J_od&qPU0WMqYz3>Kc@j6A7=nxwLh}x>!Z4P>dJz5Yg@dd!ocCcMZ?l+&o=CmpTmSJ9pG+{e>D34eMc9jT@t3b>UOP%tD&J@fh($vW23!kQFlGnXD0hh3Trf@ zoR~l0H)*Kk>0bmftpe8XiIm&{^KW|aw9-SkfXV0l?)al7OM5*F2!}_JL}RI)?=qGoq$k{_P1dRy1)}LV$>yIH4jJ!d<~2z zN}vng6BQJqk@WwbOoK=3G{X;9=y}87Ho6`t5D$!5!(Ss#QyB}kN&>*vM zB-C*d>5F^eiphER)wz*J;zty1$J;oWyHnRT-ERWCKpKzO7MGMHO_JHF*-|skl;cGo zw0eXRS&y3fO)dekUl~n=JKF0L2!%L4*L7Huew*os;0XL)>)WledbO=Cw1f6|fALkI zhGiC0azhf;eKAJ5{zS1~OuwV3x5c&mE&=+~izfXqkv_jW6#6d#HyVpB0oFPA(;D)t z^HSm98+00GjSrhSe)TJzZvJFhdj~|t?K+K9K_w=o@X3+3v_JTd5s^fb)Ymdg1$CDI zl$&1oCE$uVA$y^19$R(^*x(B$29j*?CF98@Myz?cX{BEte&aoGF5rS6qYw<79 zuW^9?%OQRSA=0;80@7jN9vDUiM*O6z0g7bB!AKXdcnKH;!Cr!JyepT0Cww=*FlIwm z8~Beh7_p*^giL;tI@{5k*=PV4UjjN^fE!}`sAv2*33ZW6z<369$mRcZ0sncC1U-^W zVx$?3A*%&2jCrW^C7?tSwEY=pJPfyagvr64mb!4WVR%EFN4(wKl8*#ne@BQ{D=z_e zo|Dc5UjpXrQT@d928bW5i9dEmpHL5ZqZ#d%W`#{LKAs@Z{y9FM*(cK2Z@jqIqa_7$ zx&$;m#>09ock^lEAtN66Ol`F?yhNLFK~C}G@qQ`sgc!%MmpCA^i{ELzx|2tc?JSo#1j@Y6S4Yx{eoZa*4fu%#+7?e-X zFo0g7pe5~#@XsfMks<~!@AQKvkBj^Y|P^uBkoznK?S6^NQ&t%Om_S%c; zGU!ROo@{;aGe3wtlK!Kurfl}7DP`wOxpz`;K|=}eY$vYw{A}23vWP~6x9ezEE>-vD zxh`jov#O?m?#+zm7kjp~C84qf789?2Hgx>rT)Qxf3GD7Z#s=>fAC4kLc+BduCQD48 z<{gZP+)FgqFm8-7l!tnhS;~f7&?UPB#Q3!hJ(z|$zv)J2A8{AluyEetdGo8exE@{o zJ>`M@F@!bb$6#uNzHy-R@S*HQfzcb1_WSwUYG?kb#}-Q_h$TYdKo-Uxp0foE~B9&;?Vy6Dao7`*?py4HfU}_}j|M;USpc2tQNRwGw|!0q5cRBc7Zr!E&PCCgKOz@w9SL6J!2p zQrcL-gGFq@CBW5yT#>0~ox&zRZH=`dj|Hiy8URhuPArl6IT-D8YcF}j#JwF-*6YW~ z^j&gq$BbArRnYqNH8w6m&Ks!dXW-b7C#>^Q>`6qv>!zAn7vxoT&|NdJbwqO*`>-K8QE;sr_Rx9q7-n_oqp4G$0 zqHv{Ze(o{gRjxOX>q3gioWtE_L7l13G{L$KzL{ebkby)dbt$f8(w{dT-5DN~Z}qB&e>Q z8S9Ez=`#N##EG~6a!ZiEzrq5#G3pgcF@ z?~_gNATv!^nlL<4_Ys2scAU$Tu;G}09zs~w-VVUuApW&Vo=;mFqFmvKe@c1$OW6_D z0Fhat9o0v8gtHrP3}JatKaX9nkJM7f5ldmKMgC`uIQ>uv^W!Hz8l|6-7&X2vBpJH< z#1Sn!xIx?V%%N=4en_L0#}g9moIJ00kv+Pbvd^wq=kHToSF6O!{JqUlQSRq)LT=tI*5=iQ=`X2CQtDzjMGtBbfF%|9E{!m0hFORCtp!`})voS|^3z zPA4?$#41sFLK8g!((6-FzqMTDXaP0XdURj`3V#r#%Q)sZh?nY7Q z>_~kHn6X{MF?I9y#jNfHYB2HF^%RZTYj334I^mwr)wc#D3X6Bhrn_GPV0Bz45Ufb= zpbZuRUnqP{hbjFok^#&B+glb(8Z00SLesqRS|1Z5LJf*tf~oh<(0bPCaTAZ*JcrJ7 zu9J+%9`qGzX=$lH0?ZfI7?AJrqv;Pmms*iulY*RNkrHYnaMJi!AMb&rx@X*VIQnh^ zi4|k0x2doQJl*Y3U50!{9tg|Kj$-NHdX~o#P`Ws`TpJH6JiY`VZ@Vx?%E{jhZJZ-( z;kmPS2?#S`d>7khiK_RI`WWkD7vto^_f$EFJ0W|HP(<>dx}++K664&sg)j&ybOrFr z@-@hHtX);aS$1o81~sKb>RrV?YL3m8GUKxy=piH}Q-Di<6xYJX&+OG`{{P`+ixa+#hLa!IUEhHLnpDNK#%2Hb@~|Cj(j7$*U3a34Ot} z(IGguw6iRa`eFSa5DWB484-}brvjY+!jx%d@i2U?dtv*WD=y`Ps(7PB~%{v-`BYEG^*-lH~ zLgPW%dS6Yfj)9sRsfFAH1}^ha`=(q6X#ipFXb}VLLf9rt_`WyaV=E+^W(I^ z&|BYVSQ%Kk_qzqDm3GoV_O8=)pzg|R`Z004QuI$}U%WGpC0i>!g??;*PO-CcC6t@B z<%k(V=lqBE5)iPDT@QdZ_f%GhtvKe>QxZ!}fhQPX8nA$IzTe2|5)i$tSpO#SeaL+( z1}$A7h}iidMh9zB80w9(UcNe2@^RMM+;aKg!@CuUkvgT-gstU{BtlvJom<#2G|jk_ zbT;`|#B9`C#?s(b$m(Y<@w;h7yX?0U+umU0_q#Wh?hoAhrTXfv)q>35!LR8C%~IM1A|rENTSoRpx0N(8}On!Q?uI)aCTlewX7&@ z@b@f+B31Vkx(oq4SV6tp-u)GgQ_F`>R2OR(;C5jw@P-V?+cz~{sneFBPImj5B`y7m zozO-8E5a)j&fFZAfc}Qm{9wf26*R|R@qy~w`!BPV@94JvXu(2oc6eTNa?d4TKG`(6 z`$BxgfhwXz+sm7Ju;c*hSv2|XzIGv0lJrD`(OdWnAc3@jrlAW^8t29=*o}6GFDfQ` zTut?E5Ih^QXM0S@g&mWeVP4YqHTtzWSvb5oEUgq-c-I(mIf~E1f>0to{MUU!OuKy7 zCuI{^@d_IUjqPTpDb^`REsoaZJg0lv(3(&R?1p{G?tHt{m)-2A8BCeFl7|8mv|dq+ zXE~}&`Qp1HNtb{+V84wM;%Q+}@wUHmV>p8Kg)?g;@j+8lqTYw~$OQ2(e=#?=lJyWX zm|NJNA%(NEOG9u1+q89`sTEA+B}Fc!iALtm-Z0ok!d4FG#4+4Za-I##Z_+A$$4&NES{GrqF#ekAR^@J#w6`4?tSZM6%tAb9fJ?gU(`w9K`GaT=;>0)bjA z?o5DoxgQCo%CC;zyabqWtw$tzC2$>~X00*b#LwzHi`x&DaTcWug_!H`RufOzJ3iBx zqfMWj0ueFGva&mR_q`Xgcjpx#k@)Z!IOB&Gy%!}(rE8Y}>?5eG$#HRAb5!UlC4?bB ziZ7So+KK}9#$N&=JESH~#G$d7`SPQuD!3*`B#NsRCr-?M7 z<{eW|Kc!ICAV+(MwDjWB9HwOOb(K5RWp?5iDLOCXIyxJdz}em|>4|Sm`hFc*}>b9x(h6WjylZzEw-8wen)wrk~f2L+0u_2W>PCTD%z(FK#6oA|FU z)K`yX%0{@g7+lX+(1*X)r{&bt7oIrISZO~@7^P1&;0b+|w7#Y|9GLXYwveM;en5D- z$$vlCsB5hyHzVxO(bWRf(u-wZGmCRGW#Y{!;MlDO>60Q^K2;a=O}1%00Fo0p-SMIk zW%D<~cbL8a6ISI!eh1`R#2dg+c>;YhKDn!WAV37#4nabR;asQ@DUi(v3)d0!823QKV1cm&8PywhRhhS>{t(}zFh%*G#Pqy{ zFDvUtGjhZF-I$(Ma?*GW|U-S&qsFCUci)NW&>x9M|2jmn51E|}S z(+Wo9n$R?jf%Xd%UeH@;z!BbNmH)cvd{*Q4BjxL*?m+sfGouo}k@=NabVh}xp4y0G z{S#}Yh2K;in%GqZ*T$X2luJM%EsksHL30|f67(ZjhMp+=;(Ia9gy|{*2`Gth zyAMNIB+uQJ=^Rsi4LM4k!m5#=4*p9(+6tlk60l!_owx)T*}Q}W!&Vo{9+I?zBkWb_ z$(X9+CE!~$v=f9Cy#(O%Nv@mWCBRXtuizqek2T>;br4r@{kbNqFq!6!XJnc#&X#JJ zx&Svv2byfek!G}ziBo|8%`1-|jO+A#l>b`0RT7qrf)Z(PxaWAY)Zr7Uh>PCL9MApa zpenH)Q*?N)y1S3c9l^x!EWg!6DG+*@93m|rHQlZ)>MgO7y@ZergHJ-0{(&I^Ys@7M z)rIk(aaA=>haQZjWE$w>IR}9-^mf%rG16@Q62JglP2>O3AHg*bVePLDHEC%``)l6d zS|BhAB!tX=D01xQHfMUkKV%r)PLD`tU#ZE0=dtbe1Q*>*ag%Kp`k9xdh*0tS7?DcF z?IC0~%|^n9p28N`4z9G;W3NC5-mKj!#z`2G%wU^q-ik&_Hn9!Qg4+89r1x#EH;l=r z9dBnU{&nxWvuW)Dl&E-LSN(ME-cnvqYRFc>-*>=p0}rjqcC|_uF5y}S^{k2=?|OE& zndkK~nxuuP%l+>6t~aO7YTOWXPXtpylGq+EMJQ}HTjP(l3`jinpbXVUhZZ>JYnva+ zZsk7ySSeR0>US>q{L_+E`7lKF5>Vf?AA*j+yh2mXLy|*P-A=?mf0iY@UOdnC+YN8# zC<%z}Gm@|JMOsA{i<9 zEus+t$9xHxJ-7s1*rBh0ubYu-cy1pO)&nNQLiPh7gnNPS->H+7#rsLfpfPau3y_2u zk(N%0I4E=KB8{~6NcdWM8Km<8WdA#sB%z)uOtS3Xb9oo3P-@;C9%V29$j8+=J9~xs z(3ez6sr4Wmm~PauY_n>-40|VcLSL|7g1$KG8pHPSN9GHXOk`M%`kxv8xucH$g@Sz1 z$SfVk*Z16%c10<+ET74M9$6Acz&9J4vw=)2-x4JL3L6U83&@kr4&Cp0{@q){I#~~? z%8DxL%D;uSjamiKf8FI1L9-P(l*}8H^Ns~Ioi$?K{Ea`_Pts49puVlqdH8_;w$;Lb zkE%b~?fAAL_b0Jr@1Ce<%9%m{C6OBgf`NHOUePaR-vh}NZ(;-^Kyz7m<}Bh>*o}W+ zxR_eJ?avqlV@$^tR|P&XG@aGU7{lf;;rVT*jVNKO7lGd?xE21eZ?1lXWPs*#ItSo} z^Hyu65wbr`hk8kIYhUo+{25Dwx_QeHS%uW9=;fu)^Bk;4DZ-d@WBU)CUl{X5%Wub- z3k7~l=oNChVs00g^o|5P{(laj5o|RMc9g-76*(k9hIEh}eX}zKoIh6Fu!ZpB67Ys# z0p+~ijusmn$WLgL*X>{Z3X3+x3PABC+N6z4QU)>I{1--3gc;&OfWi>Y5j-9@ZG}wh z`63>i{Cy7c0@Q-N#+7`(2M0VqB|&<>@G*S~x^|v_Nq9$AcJPtmNz3Ym$@rW_uLtiC z83)D9nX*8qK7EI)4bAC47!h*EOZ~C#NOMiLeD&rX6GwFBkKL61(h8Q}F2vmO6w``^ zSgb@~JiQi(Z@IJ9yTMB5*722+2HRGU{*?wM-~g+_8RLy~fWn``AMA zk-J*u--L@L9>5FI?H`pRPZCkQs7(GJ(iqSS0kv} z8_#;Y>7Okr61BMIM~kY4ZJw*HN0Cwwfn*~=g_I-g?uhjujCwLlk$3Rwc{(iG^F2uU&j0x zFZ{D1%AIfR^K9>oZ)x4h~2#)-@iIYhL+*ur6keUSc=Qo}U?LZnEGg(2KW@G7WLc1d0jV z^N+)5jlNx**KqG>h(Y{Rc=COGeseVw;Hf;>e=j0czZ^7oYHSt82%Jz=arUFolMLNR zC+xp&%>hw5%a`2EBug6@aXI1$b*r`zpWE)_Vt07AR5#vO-L8$Or)wIArYK|7_AKKX z2s^iDZ|z%~Nmw0(xHaazPT7=WP}97>*%?bc#KAl41?EB<)MO4hm0NEig|7E(Wv`&Y zi8Zc`gYyj4nERd@ygj=kRSSk6 zS)& zQ9*ozu5`>;3@c%UzP9_D>N2M7cP&}WP;a+`JMfXIM=Xq{CWSJk7l9<#>fFyRDPlRN zcoFCXPu=zsGTN2${!A1*TZs<#HN6B}SLgn6yAhkLbKw=@@%)n)OR1h$@j=xE5B4bu zvecYzbvsiFAkuP6;~1di{d*12KB&-F6Z6uM?Oct)G^f# zAP%pJlESht3rUQt>KXVBk~5>x!GrzU-RX&~GJoQ$uVyPSem|k+sDh@?Yk-xg`Pw+$ z#m9EOU{}k;W`0?Vai2t5P!5tDB4-el=2|j4-UF@BWzr&SrOzuoKE1OZAwqX#-7l5- z$D6zcz>}Lzu)&())WIXb@1+snVr;NYh!LjQZ%{Fii;s;#Dd*z#Z-_Vh^P&NLpS0{E z#9DZaSH4@{H^;MJAv_WE@@ZsmLk@t0%_PezYM*)xXa8W=XmAV)tQZ6^c!I}7jnor! z`KyDZ%Qn}pExedEk8a7s08M43-Y=73ajPM1NRfwVwx%z?DPL|5^;jktUEgWZYqjay z)=}cX?`7_685|*T!9Xs9^$p!*r`ER`*COHdk!&>^Jsd)$mBdMg zN{zP5lz>7to6umGn>}twim^T*?aDLVx8h-o_cniq1|}PjqBSdcf!Mstrj0ZU^COxe zO5Qp1R3Fd>J_)QyVJPVaow821(>qZznJ)~fP5M7Coa6nWCwO1%&b3=x9*;mFc|$CQ z3|p^?$D3TYL)ERc8I4MX8=oFt)Sj`f;BSE7uzptq7k)}S51Me0`7#7jziaudu1WHU z++@pl8Fr^z^yUfGPY}k2P(x%3g!P(UgzRKThkJdowjS}~T?erGdq|V-OK>*w1u7uia}b=N3f~q;%Hb{in{nKm&x8GW;~!<-y-S-l-AW`a z3s^+Hd>CVIKK)dK0oX|gTFqL&P^nXz`0W1o%)ktJ<=p6zKa8yBIXH6S5Jp)xHE`tF zn51b?#-?WGl78Q;qS#_W(wCW8<++XqtO;t62I`b5hic5s;nH@z?dCxFQP=vtB7P5f zZ+8Fjy@}j4=LSLmfiW?kg#pb|8xZHTeufM}LlFl#RCNdy#;8{Mjhc9$+XJzuK|QHo zv;oYGz0ba@W2+eFl8LlH5)q-!gPN`kz^OP}l8m2gn%cKopCAe8ul?`Qx3gu~mcLMn zq?v=7OiO&6Q7e7-7W6lOspj9+D_naZ_LUqSmjI_c^~2X}iV}#y#=h=Fs~U~9@6eQA zn$wBv8Rw}m&>!4e>m8#gZ>v*F+$~1kQSyG~t!z`}NmwpVoiE zBw5>CmMtL5I;-w>HyCs&;X(q_Fmb4nd0#7jmOzzX!H~FD&oYDe zwRSftGvXeP+^!#Dp%Od@&np$eKuP4aU3^GEj4cC-7>fvc+kkY3hxRv*e@lMaB%vr4 zEAk2Fa~2@2D0eJ^XSKg8MCw)3#ugH0#1SvuEv6m71z`LxcgntB=-WC~8u|K&(`tWz zc>Agy@Z)eO6&7WfY?!FTG7vWb;z2DtQqy%QR*Y|IBl-RQWDIt~=? zYP~W%b^5odYle&F>S|3+s98%v z){D9e;d}=xW9d9U{7Ja3xcKm63nx@%%(^q+%K-1n96L^Hk&}XTo-P7-Z~YY=IAauV zfcjQYKsV}n8$&R;qF{&k2~VAD1*$GFtu5eO2pp31%P2~x zemOq|{uGAZ(;VxN)G3ptH=z(`SU3?Kct?@tx;X=u9z!iJVwzsSRV@O4J)V#$ij{r@#HQ-TH+cS!j${=5Z>MA5G_#mhKxG0~E_0GW4Iu%k|_~8pi!sz}Yrnk%L)}YsW1E z1lx!e1bEj4hZ|N=FBn&^o2WmREQ}j)f0Z{yo$h~-`A~~onJ_K*4LR~xH9|KGynAq6Gsl=%*O!Y8mSUK0)J$&;3oN0XM1ZSKI>yZL8Bv;ynT?EI{*Ofx$}C2NpF}Lyd`4T4(O-4SD{n^0$Jf5L z;oDkz{Xi%kl2Z!AE6|{8V2A>&e!B;ny)$b%dkRO`FBz2Lo?mbUX_j9|-sS9r|EPe?D?D>n_6!{;x0U)_^3}S1Xn6{xIk{(s z(MKEZ*A)lG_W@Z?&{TzV!&hR*yA9pcsfPs@-|{^PIyZSm_POwlh_&EZP{e~yM|M=ci zpZrniMKh<6LlpHQ2D;JK!*#>Q)n|A5OL6M;aqaf+{s)6STM|KniYL$+tP!nbi`L*D zym)Lis`FH)AqH)je5!&3StRi?JJA}6`w?azsBBdRH~H8m<75Nf#ewtMtHr5L@3Kjn zB8;>R(+q#|$4FYMni*M({kGVj9z}{S=?E4v%$-2NgwOlFDzIcA!|Rk~ZgHpfpjYu% z#tf=b>O@spNI_JBsRE0FsN9={6V)f`YzaZH^EM!p1YcBUwbLqzj{XNiT$@|N$(JDV z9Do52`}&SU94{Z(4$*khSBb^gmexEAfO-Q zAlG*I*oo8ACwa=p#;FLw@7-=M3ro@mKGMF^B2yBITF*wUaALW(;4ASBAgblv>G>&4 z^h>K)HwsD%q1fBfygdc?Z^A)5Y}5c>=$uD{KO2M=OyvTWn0hgYX6y_6Tjbe)*bN_6 z8tN;Wz-b`^APrn!pTt%!qmQCs1CZhTKb`iaZAYLq8y(5 zjrYzud=>!1FiX_xI1oeU znz<5&`B~z}pF^hm$1Xx7hUkBFvOMPA`$!)ekM{LIgR}D#^a?IGpr!3J0hmj`wQU8b zs>}+;NiWj=g~VOk#BS+6dG(ouE*bNoE;O`zsHYR%In&u|)t-f-gw+JRDZh+gl zW~GzssYFtzo>TOLWyqKLr+$$LS}n-B>p;7!TU}>9t70c{B<2#J!Maz#-iKr{xBc8m zY8}D5Q5{lZt&uyWk4W)lT(r~emJ8BSq+0E4B3@&~i3}%Y1LOO*igk&s_)7q3H)~%4 z*3T{hE)d)uu5qaxJ6ZyzmvHZq;5tv-;@5Y;&mLGY>U}d* z4h9ncD_>45%;^>yc7z1liYAaeCdM?b+AC~x1*yOK0H)OFR;!>@XXZDy zBC(C%?KCL;mA1nYA)$PNsFj#@`@&sYPPA=|GXli0CoRkFuP`PtI?VmPsw(N~&G(w% zu)rcBjRo$etG~Js7bw~fbk!yJhn`yvQcgTmpMEv3)@ne|>eC~~_9iV`oxXdb6z6p5i# zeXkomlp;UWBu{CpJJ(7y7bb;H61;LK&NA<%;B80;Nx)h`X+j`6%9zGpA37K}bUA%eq zPn;P=0KWdEKqtpA!~=*jGh7gaL-yxzN`c?HyR)O9AHerQJ)-J=@{Q_LOIAtoA8t1P z^}3;RuR@Ch9x`)Y6i8AI^2>K-4nr+m*hYG?UKq&XlSnjTXmHL4Fzp`6L4WEeuSpSgnmp5g8=Iz?%w{Y~fz49l22mF~lTl8!q zlmoc>m7m+s6|zr|fk(%L*}tvQIJPowZp+lAVDQ@bRT!r0umkG!_yrAbrACTZ;~lV3 z2pg{DH&v(C2*F2i!wfHSYr|X4h4$}9czZsWQ{;YE6UQwQzMt}k!$^u{^&@`+^#>x= z07UI90qN(aZ96F^do^Rne85a`G!1r~Y zJ}dT5W}>h9S=LpY!GH$-$>Dkr*{;vd1O8+p1L0GLDBhk>cp^170*6J9B;kG5c#RAc zv>$sh=I`C8J8)(&<*j{)yyKDbu+H;qvtcrb%+KT9IF>~VeYesX7#FHr_^4`S2Ss)4 zOJbphmAkKWd!!2IA>#k2zi=}Vta+qxLZTEYN3otO*E;y{zJwnQ894SH#)O&Rp!{_e6Qj3J|Mje{1!g+KNDvlN(6dgcumjHflE$k^8*w266K5=avuU)3v3Q}U) zK?_*T?XTW2ZH!i;J)v8FM!glqdU4yHG|eV@eA(ljhKul6Kr2=2wB7x3Y14N!=2|to~hnH{8CAo*na%m zfq$m`k2YpkQ)Wu+tRYTXIPf%Uc)#tZn%7cs*^lD{S|)_!9=+3jkw9>{Q8((hP*6Yj z8(p9K{p}eN%SG&r5=);sWYIpV-+Bl|t>@rA&viP}Z__5-mCqdq#s~%7ZycoZ4n#iR z?%-;iIOzSOoFjVhScyB3=+Pm z(vW7o+f==<++Bv9=o?K`F{sn#^Q9L+=<=5{_2nrCo59tEyr#Y04|~o&Vcv`iwCJxs z&zlcZn`vtu#{dISE8Y89oau<#z%A#|yvbJYBXQwY+#tr(9rH7JYx!c)x}m$b^c?I? ze!m3}K3QN3p$If*}-_(r62cVYo zPx|j=Za!c{xeusghoAqLe(UaDPhi)cSiS_9OgSY@a|qZB3;p>1_?b%POX{})WwQLV z7PAXBna#rN&#%5#Q=n_wR=k+r>UNV~>5|!7R(B5lsW+FSgF1KDfwd*XI`LsL|LEoC zzlvWp+o@D4jSh^}JQfxD+BW2P)IrEtd31EXZc?jHSMsJcu|>T?+-&dL%u;=8?p|Pi z_oFsu{gSYP=dtU$f)uy86hytXG=}c<(NNNDmH1^s*w;Ra9ytp6_n%g{__9c3#@=wo zcrx9W$#@=)ue6=%ZjH=lfFf{=S4SXl5BlG=yIctxq4EBHiwdcoE&NNSeOg{AAZC{kQZqOmR`>s_PhX`Alv zpvX&FPdT9^%k-9;JBKkcm`f-*z!fp9tY47H`6>%3{g_uEc~JZO)w5rBIDd1HGTNhi zw|rd&8)Wm=gc}xgFv5n%(`-$T2?OS}1_)UU_)cZeEZTd{I(MC@` z=xS}q$k=EsfS2D)(y6)oEy`q_+3D(&&Zf6Y1J+EI{0%S6cZgzy+^umr6@!Oo`sk{x z4|bWH-0QVj6@o4EG+Tmv4QJ_leL-)nY%T#ZTjT4!Fp~>D(3;d-Q(pm1sA@jfL_0{(_|G>v00R2@>{|CFa{@n8-Ve^;1xpTs~2BG18)tu^euys%a54g4w{GLcLq!R!dQRxNHe19T~f z;>xYPm#M3hYE5 zQtRjtQQE8KJ%@q3*}MM(**SRjHyB^K2dWN!U5eW32Gu3^tNG}=rlDDSo^=;5+1Myo zXuCDk=05Ggevw%pTX4wJ zJw~mWHgAYHzeeVg*p!*8;Dsud6`o9I9>^qDX{9x8+c$2OcRg9krEuW+jPuDJ=(cOV zAaNcAirz1*3sFFArZ_vQ!#gtsZlyciWECIr(7RCh3nSU{WCmy2E7g~P56C}1-YJ=< zdmDZcnVDccGU+YZOhIjR$t+}qxM>?CA#^n;Wt4MY<)%gTuYn%r+1r)r=~s-NOEeZ? zQVd(pm2m#u5*-xiK=Y0hIvw7?7Ylpy$=KUw;R5K9P6A{1S;`=0hV7n?Klx&D%5Uir z)wn~Tiy;-ZsDGIfy+2PKec-3B#ynL#c~=ti3AyGQs&j7V57#|^#6#{ChlH%A+A>X@ zkW#zB%D`m$=BN^l2o5Vxt1q3nb*-%eLY1a}LkAj_{pjkjBL z#RbrYuzcq?6J^uW>aKU%GDL1q5o3;Sk(83&BoOhwEfRWQW_ffJd|AYAmR7nK$X}K3I*CPOF>P1JMn(y&PSNHM;1_*78SSfoEkG)67cnrPqd*CN2UW*=eu{ZZsqMhA0 z=F(D&eDVvl1^G@doKZcTvf`4Y7dROhpXvZ>#E(QUA7`L=7W4!>BfYU&h-+!R$a9sP zLh)il6AZZzBMezq=QM@ zNzlf2F}y`Ec7VceCsmOc-GG0Ns#xiCb*Nq#zFTBBL(b&v8*Y)&X@t%tXCUh{doN>K z4llTZe}r1wT(M5>HV940TF6?d#}`H~y@QIR^}Y->oY-$ypbUm35QQwQcJGrLuPpYH z5*ua|!$o)*_3+|2@p~78>B2=CR!dEphmV#>ulZj@*{(YhcjEh=|- z{igIMUTgy+VD)Asem};R`+XmObe6y6e5J%lX_a)p-z{#>pus}GD__b~aS;Ttzu-dw z^9d8Y@{K*JAo3Llmu>#je!zOr5r4L~n&R_q*JD(uE!MR}$pfi!$h>$U=vGA^kpwTE z{}LuHkHdUM8$H0}Goi?PuLA!c+TJUu$>{AD1yK=cf`asbbP*7hUSg$)fPm5=0#YLc zq(exQ-XkC&ND=8$BE5&+1Vm~i)DRGnmH{eSf4)H^ar>2estcF!(dj5;AIF;My10TG( zC{T7fyPON%Lt2p_{b2rYgkB-J*2c*-M!(8;+~Y5D^CWnycML)uDg6C_mX#(+)gs-p z3Gyfj4C7wB$9ObI%-;Y0B>PvKY7W?g?RhOEljNsaF_m5TcahRzdH`KXM+KC**~wt# ztn_m{MojM-T8K)|-(4j>4?0Uo?eqCZ#|~#+1}77H9GqZlw#^q_1p)Hp@Xt!+q=E;NedA%$ zuV4+eLJ3c#2g;7W%Pb(I13|W=kWDM?QrXHj{Q}Frpwcb}mgwz$wZwwY$4LTCSM2p) zIJYj%P98dw)_ZnijmfCq2EyHsovhw&As!kr&U!lp^9U}3Z_f&%=0W^X3fY8-vbYE- zMQLaN&^`6!zPve+_Htqz3CZ8ht4Bw|sMbWs&PC4pg|k^lvDDHZ0M2mf^R?B0&U2_{ zMG_>DdL{QdIkpfkm*iLy=wpzi0$YE85;ds5dt~y`*CF#ccz=AR<1y_z?&D7yNTbyu zzGvo0E&RL+9Y%XuuOBb8#cm2=2#gCU99J~(PFlJGx5<%RrtP_nH(Ux~0;T>rb7H27 z5ZM!3Lbm@0DjOKL*`BLjUaC{>HQH_s`ZT$XeNs<)GUVap#?w9Lrsn!AOk6O->)uNX9#8K3BQngcTcA9M4as)9J!`#j8h z7gdukLks_8>~g08G6{`4b1$kb2gZbtFN%2~*9zJZF0`Ktsaj3oKMKYF=zjl%lGxM> zu3J=G5pRQbAzU`8%$Co42!C{3?we8BRmn{Q6s`i`9wCv#+Q61$P~uNQZ| zX+EYy76lLVedaezGrS_^B6dhEyu58_Z~iES&$LKBbzY!N zTYmmYg2dD1NItV`ZbX0?Y|se~3(F|hH(EP+?6feMujG&Wv+gpve2k#{3J~{$JjUPN zg!f-#kc7`^35AW|=N-%QKO2CH+6L_8!a%dfT>nQmsqvTk2l?L;7)LbLDHM##_3_X` z>_sBDPxK$1)DagI|KEMPf_L?Q5f~<_VR2HQ*yljxYXho$fWD#pCZXxX{2&i9v`*!O^qvGnBcZ!zEEnd3Zj&)RW)s(VzW)kz$b3fhVoxD! z6>y!}%U@?N{dDeshv(2A!O*dIfF5G#aDl|qH1@-wqDG<0x)1)*$tv>i&Rfer^%CT|;hmQrC&l^v*JTom-qzPhzUln6 z%&BsPo}5-XlDaTBu96~^CZfaOxfXk0#F41^9;|=pOEW>bNO$G^Vl@18{d<18fUhwzQyGGN%z#xZ(Pn$w@8C z*7A(fJDpug!(g_;6$}&Fp3F{&LGZ&(i8`p_fM-O*+9K$K;j>M&u_;+Oi&BA|XR$G0 zD>_;iKv;q3mdL5S9o*J-&21eW6R$iEhJ`7az3DqY9*O!S%P9E_jtN3njb=^p*lrLm zvkn#w=5BKdAhr?`zr?e%{`tshz;9{ooDSZCZaUV#LW@W$Vf}Y6+q~m+SW{};LiPdW zqYjDx9y!C$r5Nd9N1mBhY{m?B70TX$%568Z?uEB9bZA0JjE~4LINTDNPj;ROy}xff zCB!e*n3xry9=`MRL(eXy;)0YmHL;maXrE$0`?kbLqo<8&NkDkr%zdDH+9>BsF7` z>HI{GU&DY;L4Hb<4zWsGwBwvsWG73?Z=WL$8(3P|TCM96N8=QFp6rZF4k9-z&|_Z( zH;E;O;bessB$Hc5Eq2R?_}z6N(Jx19?0$95;kJj4@r@(%6cJ&bZHFZ5<_vT)GeN`r zAd}Rp-9*X&swsv*cdt}Ioxbqr@AZ3MhsLw=D@f8Yy?K>;vt0&IwMEBfeFH$l^=+;I zQN8jQ&o+i-&)457D2n{=>8KN?kPB@u=<*p1~JuxgtWqTy(Vnqa-J zF%AaW$3Jg)J#2q0bLpk%C;nh;3*3uXD5OH}g^T!9Xg2nMXA2%i!u3nv=o*-ta4ald zI}FM+sBhyLI;*0pVgCDw%4vj;Dk8xMm}=6d74a>I75s9WjnJ3g;z-yc>O(1&e50s^ z+Y9bSmlXsC9pS`JPtWa=WkGQVpxH(m(=icsc!$`&P^XqZv*nB8{NSYT+tD(9W65;p zWt{A{y}WH`dEkyXZirH9&lAAX(LdL!yDQqT{#sA6S@9$X*v|H+g(YMHnXM-+G!Wt~c#U%dHH`DqpC8!-Q${1}JH|3QGJxhD_T zPRl+g_xG%%sfg0@TfERKbF>RUm-}C=A`7L=y_$jS_^ITKzVjyb>WypLZPX zwp4_CddhV(uE8%M{2yJO{69KFQ*b&a-7>!WrG?rCZkhm`P=ACJh0sNjBk%IR^A|nU zlC5iOnsvJrSv_P$dNy;}UFD-f^4mGVKj)`4up$u7^kW`207$+_+pqz%mC~3qVz{`P z#X3g*?h`ak7Ll5s0Ww0DH8<<(NTaK$=rn&epIwN8+@}<#wxOP(sa53l@%U#UF83!m zL+BV8%EV5dKgWMFpfDb8J>gYUH|A=p%{9s9q!)JFmhWA%ySwiJggpu3 z1;qr!Pnt|~g0}g0ugpI3qcofwQb_R5POX0(3MH{$e(j2vu~*=i9J{(!A$#1>M2jXC zB#NVY2sg*fh4jQ1X%m2L39%nVydS9E*JvE*(#Mxi#GoavO~MWt8*U|?tN-@W!;*bI zQ&$9IHrOAWXxBZ^iIXU9i7#3#f~j|oOWQmg?j{t31xc!&^+jh$G=g4HZio~H6)K!% z5dT2b9-mrI@EZ-rG&-)R#~iRq_Bl&Yb;`7jpH#L4)JGcjXPhk+-3d>z=9Op@7&wy_ z?DIUUL^43=3$9V9q2c8Bw#V#2vmH|UX*ttV!$M!vjjCAvf`B=L=h%m@fJ~>GOYS($ zrkAMgtH#ofoeW-Ld7m~Rp5!m8z8zspoRwMkxff@?T>&qp2er*wZ$v&nxk#4mT4JQa zg31F;LKZYO%&aQg)K)odTJrM@E$_NH8m9{r_^sW#hE^~Oq~5Vos#>a%TiTq}jEGq3 zUd#Q~K}i(`NxFkRct=cNw(Jvs4*M_CQgv${{wc@;ou6bkjh8oh2I7iWR`X;J+E-?l z?$8Kp3kLEBO&Xf`-;6jvgS#?k$7Jtpok7{pX+|sR3|K8h_{_Rl*)IC-p6{Gdns5>F znCh(`p+y$LlYf6HsDC~~)owV6j-i?=cE2ZXgi0DY=qTS%kSW`(*6R{f4l$4VW0hE# zde7N&il67)^!SaNiIS2UhfnEFuvj7u@;A2oc|+owtTN2}NTOg<2^G6ycg@1p@QPhc zonzIwm3(`Nyso5Ab9}G5?8s!;cwfJcNr=>62}~zBuga~19mDMkVd-l^+0;PAX66OU zPYb;Hm*yY#sHW?FR{BvJI%Sb*G;$Z6{g2Msh;25Ivf_a$2zB+OG84;p`N9U%kPfp7#?5&z&4-t-Rr~gCRi! ziip#z&1g;D$BPqRq4%z-uwaETm_0+9wW%LRa z9Ynu%Yo)_INKm=_E!mOart`=W`YSjBAv3k!8cX}BHO4!1pg3)`Qkc_4(SKMbk7?!f%+2A`5_;if!GkX^zoum+Vjh_gJgv*j;yaE|`P=Xy*GN%^ z(b{I-N~X5gDLU~%-3s6|c<-Vj@dTW%D8(NTI*;(<3!LnUmsp=#?%ZwdFieZb55m3y z2vAJ4%pS$!lmfcnm>~avQNO`CVXwI|h9*2BYI^4Z!n+^(tD3OHR#>53NhevhkW4M( zUr~|M#ut^UJ~I#y0yGUDc6ek-w)Ai*ZKTZcd6o)L_F^opEhcMzF?Arj33A(}u?)nCqt zh2KkT8tR%9f!Uq+#; zvkw_=37B47(OivpIu06;&8qH*ZwcrL@QL33`u61#J0pDQg!&8+fDwP|H_viumK^4D zQ{HUJp&8kylIk>(XF}A<2ypxS2lm=bUpFqDQ}v?WS%J$b3U+g(*Oz_osL&0zOjo`= z8+_9f$pP^}0QQPn?lPB^p*GWy!i65EFLwF?{0~iU)NtPGT4z)h+$@PGZElt2ZlsI` zwsJ`INgP=TE_7^RiI;nq>cA0eB+0HZHmJ87gdLW^teEv)4_traZD{xfYZ!gZATQl= z_NggFlX~qmRzsa2AA41)qZSh33yQO1`-Ib*7OvNI$sOJ1UcG65=0zww!~P1!w3VDkVgzi_5!iklD6&dRbn|2&{^OJM z{t4Gl8G~7)4k=D*7L6m7d&DRp!pvj+R!vWQZU_F=({-ln$lM#9~reY@xXIYT@FqY4Cyy5os472$N zx_R?eWGvVL#!EiZCL-U|Q*yxlS&8go&@QZVF~ zow=Dlm%%|xN6|@6W2eV?epXZ4Xw#s-3u;SGC{My@E=@^6o>jsQ{>{Q)9^S-cW7DWX zIdEQ5#&S|V9>!wv8%nYw)olPTc$~ z=Bp1=VaB4&)Uc$D<4NEA%XlhP0h6z1NVP5v^6j;xh5xRdWD!> z>M5HHP%D4#U0PWU*kdF8HkU4w%k`@DGr7Mn-eaPV>Ggj1T`d*og<>H&C<$xrHl0^4rm6>oX}04DUh zcbSuMYT3CZzJv9T8lW-L#2eMk^E!U)YE;Hm+e9YeCN`Wh7;6T&wL?;OZA^}KxoUoV z`|Q@Q4WZsL-}*=AjaMdnlu~ckll-HrBtNdWDh4?$xK6BRONpVsv90=FmkB4m*Qy4PsZ(#Vo_qxSL22M)P1t?46x>%#T~L*i z{kd;?()(KeD)tHav@?PKxrdhvk<18ja+0PMIAZB?n+{%9$7W(@t*$YQ#!=Fv;Vt#% z!+Jwgulk%%OsCkM&qT{^1bR(5avSGj#{9a#?d;dUHBZ&m1i%SqG%(CR>O$j)cP$91 zToihrPrEkCxk~X;JwSgd0+S66QRaRfUo`Ao1YE9zoHQmYkjwA#>WdRem!A?n0j29X zxs*wh39&iMQcp`{o#?NA(_!qzQMS=T!5PQ!qk)+R*YV;8rfYS7$)>05#X|rrXleR>uI+hC zo&aKWiD;0AE5Kj-vj=~8+GM_hDYw5hT09^gxwoU7IKBgvG5x!1Zr)so~Eke7w$=Betv)N4$R4??6K_h1Ft354~;ew zH<3oKSezt2!oJ(QLxf=%PRkXK>*aaK0o}hQtkbKBh%fJgM1{{|Vn0-i(GT$eY=`+H zzG+cy4I=DvEGxATTu<^83L zCjn?Ik|WTao(M6Y^sFEr2HR!Gl+*Vx(XXRLq7e1u)BO8)b`utFv+~;e_M&z<)|EAb zzW<@F5!}ZM31h7p(BETiMME+E@1GaI22UUU@CywwZk+H$x9OBE{KW5>9#$a&e~v4<@ajUV(5Tmw`Ht#I=rK%AGyfc=kqYZ}ut-9*)ggq<~Lv zflA0TKX0t_Z(-%G>HiA)*(S&JET9lW_%})YKD|rzSy4~V>&@d(+;(?i@ZVg~VE}n~ zJXIKY+&cpoEf{sdWAjFv()ok#rMNiE-d5=M8LJGwHOhJHurjVJgwF=8X_w+`mS)SM z8xlmh@i8FP6VSJgb2)D6i{=1J^B3%qBq(3=+Dwag(jh^%rND*vjN{3=VlDTFufJ42 zFZx-hzk_uE;Wq?0%4F z@HLM?2C*gq$BB0N&g6whFZbsLJv<$n*g1BpK3wX7U1`(&u zMO6R~C@0I=UJI45nU}w;`~5n9`5OL<)J zhbYrlCXHJxs1NV7jYO)us?wL1GkTig3M;S#y(`iCP`9`_YONa2)x5_W`7sAXX zH6J0K(*~Bf+xUyAFT$4@6XbBP3-A}2%aIT89=~vOp@rMmEN?0$4|4`;V7D>=DNUp- z(Z!Zr#&{T7RbSlY+ESbu5CE0M*IPT7=;9Lvxw6cJW0`Jzc(v-#Vbi$+JEu-r3g4SQ z)_A1p;c;~tevSxQMknpi#E&sI#*4>q9MpL{uxf8J34^XT#o1pGC+GaoMr^1*W1UZi z6ahj2gWJX}C^M-BzVwDKy!}5WdYI{_HV+!Pa$AF4I$s3$O=`)QaX2lG$u?0w>c2c3c zYJHMn2E$IWJ)qqoHMEC(uGu&-i)8#+Hv_)mKVGU(7_C8n#lBGW&!f1bZHvYQ4e!0w?))TymF(5mS5Ym#6E8!P{t#@8J@QW8fA*M;&}G z>YZQDxgF?i=hy;3IM^CpoCEFTQpdSkxBNf#% zXre9Bk)M@`o;Ve&eb6lTJ{}OmfVqagz>T3fmpc#j6&EILk}rDhrQZoEeDTFZ^W&l1 z?G$AepJNckn(91`xed3$)-NzvwqvOxg>^GlvTJ{ieQYaIGF@7Ihkn-Tdh@M|bLe%6 zu?Jl5Y5CbyX{(}sGPH%>EZ=Jl77~Kl-SMhyy`DNb?&p8tZ|ARwVBbtH!$Vy?pLI57 zmggkLQ z;$X9m0*ONxG+2t6z-y(~8p4!44hDe}j<-tfy~0GzT7nfsyyW*_BCnc4JP`TWjx-%Z zfXMNWnn>#jb+&5wEbXSI52Dg0S4XrknP?@F?!MObjg8Z8qu?pme<<{e=uu}<1Xjj@#hD?&xrM;XDe59pUfQ%3i0yqO zFNpIB(C*nkUrRM+gR2%k43crulhi*pot`BRlZK!33&w9@hMzxAG+N+G-2l3xM)zkxO)_LssK2euXZ)sgj|oPz&5 z3o2YA8`6u40n(y%q>LY3q2KPP9qZkou>-6-k|Z?0OqRE(pIW8Yzum=urRmYdd%TS) z5AqHlA-(w!`7Y%av+gbMa?Pl+i?(lXMhdgN!uSYbBLQ|ht z@!($8PF6a#Wsa*ju6C!WOBgcAG-h9h%w$k6Tg?%ok}1lTWK#kxVz*6PKHN{WZkMg^ zi}sHN%=@^=v&qei)jcAC`8TfAxwFk*(q;V3b;bDZx&QSM0QW9|e&=082$Gjp%wtw( zKL=jbZ<>}nALQ3~AAfba@CDc1)fBp49nt7Gj3$;Q5lHDnsE{;!Y@mzMQ*w&@6P7WT z=bHl_&5zvgOTI|wU86|HVt&8~)M&w{Uo_+k$=u-Cj2^WjOy$ag|DU<1Bn>HzaTn;T zVQ&u)5tcJXJ~xw0qQ&k;eC3}E*!R^FOL=WTC;q=Z;D0^ne@7n45W_kE8fkeh?^yq6 zOCJ!`0H&3V@ZKx*`$kmm0-8zwfAHgfbTvII$Wv)6Y6E)ezbBji=UTYBnGV2^4BvmL zy9GSDEj7gPxJV$WEy58MJOH ziFx?bi7Rt$sS%Fta-z%#_2U{yJhM5)EKRVV zLzzKrm{04Wv;VhpnRE7U-vpxZ%CrkT6no{ZCCZ}*1>}>{rDe(9WVeFAFT8R>Qu&l8 z561jn<<{v@d@M-iu3V?eDvH%*hGjby~Pf< z?3iNFh~UHI)iS#EFveKgNN`5%ZX4@zC_`VWtx@Q5jMc>9l5s$g*&jyNXSWq(uHS^H zDchCy+@bd>`B)0JH{Q>vH#Gc%E?orpFy|zI#_Lu*~c1M+rri-+7iHo!Akf!A^NVRXnW*1MZKghIfI{eW0@ycTh1*E3T=dO;`0n_8^Zt+mmIJ*W9{cS0&nHtsXW{55cr zgZch0Qn%1!i*~teI26X1ZnL)z2Eyuy2D=dm7QpnZ5E#7Kf2h`6Dv6ED__1$gXb&$IKUC9*ly#gBZD8&mdd0Gs1JvHhE*Jktr?^6u;99%F|bFHC7 zGF?{AbH~9Nd40@L4>Mp`m*|oI(Q7|T&r7Mls1tnluS{WETe|B5*_OzpOO${Mgr_k) zrB5xcCHB&%rY@EmKcc(xe|-KcoS&=XKjHjFyObMRKsz&$j{eg_yI2WDgQ-^-4ogDJ z&MdT<6saB1MkkR~(BuDj$gcliN&G*N)GO<{jm@Xc9d3HOF+m!SnSkRSkq2oH6(UW$ zI{z(t>?u1Df{bG%MNfXbZDGgJGg`jHaqb^o$}_MSVrb=(HbixIj6C&uxR+%v%O@c? zkA#Ntv7&`jlQ*8`v3KPPB#$EP+m3hvSN+;i-s-oN=?Q5#id%^fYpUb}pcj0wpMP_D z?=(~Y(e<}CYp6q~s$~sPJ^nhpl35u!j>vT?X3we5%NESgJ83v`B|7MVTo8lCNfIXQ zq@E&(Ni7T1fC`D%1Nc;mfc_mPP1 zeMeOy*(Nm&$@Zu3d|jOD--@zerkA{~>?A7JQw0RFk`dr;hkcEfXK9wII*V4jp1J0b zSPJ9Y+?zj!_WwLrP_X+}(ky}P2`%z$tF%1^5`c;qImxUOvD22*rZ3Vtb}kiPY-c`t}qjFAm&CD?#*2%Z+=>G$f=^C{3Dfbgc|enRze zZC>(2z`YaJNBs1r7m*B?InB|~Tt?v-yKuWvcjh2GWQKnKnz^|-cQ4Jp3RBzu)E@5} zUNubc2;g+|)zz85K(|s0>|EMEx?a&fKCbQ0c0~ujE*y9>SkEq6cy5=_OM%izZIr8)Y_$Vuw?63+c1Y)Ke7F|>MQetU@fR;TxM9{VcIBb zmf~*h7KpJfU)L#s%;@w|no8{owy`pZq+Az``swiQjFv+}^Et!2o&BtE zO_HP$re@+!$OZUjyr4>LL)Q^XMQH9jhtBIqz8y@FR4bu8{OH56)!$b+qZ?Y2SFek{ zzSdJMGV!RVjIwaA&mSRU(FkqnojGot9Os^8f8dQ;$QEj4dwGbH$zXikz}@!gkB?>J zX274Lc8XwgG>7SkmvOIlUTvER)W4!?_cLzDJ#}KN`Z$wxTRSTh`G}~L7wA+?tf{pN zLhj*q?!Rn;!3X`>@5)^6L2$dWUnaqXRJ-re22)GSlDAz=wj4_{}wSB|lXH0rFZoc=S~_Wvl7-5j7*6 z{Z;e@-0qdv6)D$M&x(ZR>t9-IQly9}C`#Mgq?wO$1)T8KKj)HU3doZt7f}E`ehJ#=+885F5Zb8fH6}RKG_iF2sjD z5oQ?d3(bfQGP|C{R}AJ*4Y7IHycSZNmSWuKTG@u`MZCk@e46W`FE(t0s0>5Z7|K4r zo#OGW2y3ZHoeT!uE{Tv^@K-7j0a?|vzp3KArcK#;(s8nIfGuJE;Ajjp_!%%+R} zG1uE3w6r+3ZiNvx#DTj&3i5B30>(mw=?;d<^@)E4UM>#yhGd<Ei1F;B7w2*}&Ih zFNAb&Tz9R5>JIjOCE&6uS^->;bh%mv|4~a5=-TxhsI~fe_XnuZIi!nx1fMTNNbBOl z*Z1`$2~_o)$4R$^Dqj*dzhO%aN~~8`rdY6O`1uAIQXXFILRcip5YX(D$UNS!ErN@i zpJK6tCspT2GJ-eO&%R$GiDE}VH<4Z49h_P!fgsw&P!a+R7|(eW$IiN{MU^bKUahUp zF}o(WRZ)<7ad7U*$G76Ts!w&`6xh}~KW(xm9<_byx!%iHAzvE1E6;PMMgh|VukIY` zIWY%{zAgZ~dkI~NUjhOr;}_MNNq&o&1wU;9p-**htTfK2ZZvS$Xj>M=-T(Ie&AZ^Y zaa3E>O7!+^JdA_<5x)y4UXqUN*nb>w!bDvrD&#EUqg+xen^fcnH3*;3d(5g`N~<4% zU*nTFz4Y*?FWDpw{&Z-v6$O#7bdnq}=q9q-h*hS8;#pIjv)i#O79S$@za zi$|s7jiOI=Wy04IvbSHY&uv0RmbRuTN>p=lCK1`$!Hu~_o6l7XwYJ05#~CQ~JSI|= z>1nEr0xvti%-C> zcl$D8_)4(l8Z9jTR#{-@&uA;1uwlUf2X zo_2LTBC^^7CW7}6?8mJGU{9CE!;kYa#jxpy#Y z!n!%ZaifRLF3{2(e#;i_L|)zmuLmsj^%-K96mrR>fSDi?%t*UU)yA%mrlFER=Ky7h*l13qhpv?HN+mn3%7H zU0_6yzuw!|fsYG zWh6s4VKG_HQB-M7?Fl$XzQtgy_g-R+OA4D$W3G-U*RNc8Qf`4yDb(sg?;9(_8853j zYt5qe?z?@KFY8@ihl@%NU8}B##ugLvV(Mn;XT>hVB4g(|P0oj${~b8^t7hQY`_g`3 zP}WzDNVOAa2ye?wCR?VDqhMQL9DmTl2bO? zd{itV0a=^Ve26xh`I~6*C(m6-2<4HM&jZaLZgYC(VKK)0D$gFGMD7?6y4FS!t>EDiseIr<* zYfCl(+IL}(1LIL~^@cVw=AF6L-=n;6fa<-}_(QS3xhBde7Q*eDT+I@i5Bfj-V{1VJY9?~v$-Ii@{RUKDb|>wL1WI0^=*IP zUm$3~wgSb?oS@$zu0plc#^u@+@1zg8`FU2pYRKT$^X5D5W9h9QV9Rz1dhxmK zOW=v8oZep+lhgkaTs$V`9eyYZD8(73wdn4NTdt;6uIQ9R^a2L2o>QV|lvT=M6~#*t~C zE77KMC_Y4Rl%sKJRkJoPhEJ zn`}puTWMz-Aw+haNC*5j`m+{weCMSr->ra`e+P`?ju{48oN}v4G#>Q)v62?+w9|7& z{tvc2dwDRx_Y!O+KXhAjg8c*TZfzb+ell4`BYwa<_CldC_-1 z_S*@N0Ag>Qgnj#$3GTBzY&eha$`zU8GkNF6V^K*fen8dgkvk`vgOr{)Sc!Rp^L-WlN zpKapvjq&*PEOX<9pC`z!7{t&O`)T-$je!3}#{EO|1KJq!cn54mlfXPAAqUX(h*7n~ z^$3Kf%^wh(hSTT%8UvRgdv7UI<&zYN#-xw{O978_;-VUUXW5u(LIxDz;m03CsAt;$ z(Gj?QJ2l%n>633!l5Cu>apn*dkANE;{BMe^YWE{wyEf4_TFb5z{D4}PRp@1@6sNK8 z*{DQ=eYY-9NA0q);UHV>a+VJ=^xh%8==6Zp+(ci#o$T)Q9%#+WOB>Z(T^i@zF07Xz z(#Q|5yjoJAKw;I9urezdDcX;QT%*GbaJ1EAoPnAHb4+&XG}qnJXHz7dE9x(sa)$Hp zi+NRev+=sL<4iL%B>v1Ta5_|8+hkumtr0bgB+?+t<5KCsdZ}jAr&-9zh-{Rwr#9y^A!M0Z}Ee(<|p66nO zq+o1G;583!O`&<7*Jbi_$^sm@K3NAh(*(qMWqm`aBQkx4w~QY2z!@YXd`GRHPQzI; z4%KGD5f~`6^0s^WzO_o=45WT=Qp)n`YY#1J_DrU;e5b>wSxBfpO2`cu8Ccn70I>4` zeZhYdo-SD32;;4=%v3gvd{%z%+WJmn+ad|v6 zzspc|_ukEUkz-HUr2?7P?N7^JYaqM9e2N`(aP3Jg;h(lcC4qT8sfvj^Daqkye-GE# z>pkvgn`SeMk z(qSkEj$k`Uz+mBWLtOKFUrwu_ylhu=P1n2}VefN42+UBuC2AVS3N|*O?n&KOc$U>Y z9T0Kn;KszI!*w#(%5HdVAi8(y(g8SnkE}IoZq`1v^SSX+t>OLQ8xe79 z$WrE8WdX(HkNp@BnS!@PcC}~?L+5gYPSh6?R@r5*HM*n~`>N

njM;`Iwp%o)%Zv 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 BWp4lg literal 0 HcmV?d00001 diff --git a/modules/alphamat/tutorials/plant_new_backgrounds.jpg b/modules/alphamat/tutorials/plant_new_backgrounds.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cc8f2b36614980731481a8fc38a27f5f5623faea GIT binary patch literal 144052 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ˇSinjLtXDt+RT)cj3e{&%5n)=7#FT{@v{6f(6 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 literal 0 HcmV?d00001 From 97be19404fc6dbca20c37e552f2f12946509975c Mon Sep 17 00:00:00 2001 From: Yuriy Obukh Date: Sat, 14 Nov 2020 19:54:48 +0300 Subject: [PATCH 17/29] fix win, vs2019 build. missing header Signed-off-by: Yuriy Obukh --- modules/cvv/src/util/observer_ptr.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/cvv/src/util/observer_ptr.hpp b/modules/cvv/src/util/observer_ptr.hpp index fef3feae066..68d1a80f445 100644 --- a/modules/cvv/src/util/observer_ptr.hpp +++ b/modules/cvv/src/util/observer_ptr.hpp @@ -11,6 +11,7 @@ #include //size_t #include // [u]intXX_t #include // since some people like to forget that one +#include namespace cvv { From 52f5f056ccbf765f49bbdbddbfc97f49f002db26 Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Fri, 13 Nov 2020 13:42:51 +0000 Subject: [PATCH 18/29] text: drop ambiguous call, fix bindings --- modules/text/include/opencv2/text/ocr.hpp | 28 ++++++++------------- modules/text/src/ocr_beamsearch_decoder.cpp | 12 +-------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/modules/text/include/opencv2/text/ocr.hpp b/modules/text/include/opencv2/text/ocr.hpp index 0137c37a8b6..6cb23fa4ee4 100644 --- a/modules/text/include/opencv2/text/ocr.hpp +++ b/modules/text/include/opencv2/text/ocr.hpp @@ -474,40 +474,34 @@ class CV_EXPORTS_W OCRBeamSearchDecoder : public BaseOCR @param beam_size Size of the beam in Beam Search algorithm. */ - static Ptr create(const Ptr classifier,// The character classifier with built in feature extractor + static CV_WRAP + Ptr create(const Ptr classifier,// The character classifier with built in feature extractor const std::string& vocabulary, // The language vocabulary (chars when ASCII English text) // size() must be equal to the number of classes InputArray transition_probabilities_table, // Table with transition probabilities between character pairs // cols == rows == vocabulary.size() InputArray emission_probabilities_table, // Table with observation emission probabilities // cols == rows == vocabulary.size() - decoder_mode mode = OCR_DECODER_VITERBI, // HMM Decoding algorithm (only Viterbi for the moment) - int beam_size = 500); // Size of the beam in Beam Search algorithm - - CV_WRAP static Ptr create(const Ptr classifier, // The character classifier with built in feature extractor - const String& vocabulary, // The language vocabulary (chars when ASCII English text) - // size() must be equal to the number of classes - InputArray transition_probabilities_table, // Table with transition probabilities between character pairs - // cols == rows == vocabulary.size() - InputArray emission_probabilities_table, // Table with observation emission probabilities - // cols == rows == vocabulary.size() - int mode = OCR_DECODER_VITERBI, // HMM Decoding algorithm (only Viterbi for the moment) - int beam_size = 500); // Size of the beam in Beam Search algorithm + text::decoder_mode mode = OCR_DECODER_VITERBI, // HMM Decoding algorithm (only Viterbi for the moment) + int beam_size = 500 // Size of the beam in Beam Search algorithm + ); /** @brief Creates an instance of the OCRBeamSearchDecoder class. Initializes HMMDecoder from the specified path. @overload */ - CV_WRAP static Ptr create(const String& filename, // The character classifier file - const String& vocabulary, // The language vocabulary (chars when ASCII English text) + static //CV_WRAP FIXIT bug in handling of Java overloads + Ptr create(const String& filename, // The character classifier file + const String& vocabulary, // The language vocabulary (chars when ASCII English text) // size() must be equal to the number of classes InputArray transition_probabilities_table, // Table with transition probabilities between character pairs // cols == rows == vocabulary.size() InputArray emission_probabilities_table, // Table with observation emission probabilities // cols == rows == vocabulary.size() - int mode = OCR_DECODER_VITERBI, // HMM Decoding algorithm (only Viterbi for the moment) - int beam_size = 500); + text::decoder_mode mode = OCR_DECODER_VITERBI, // HMM Decoding algorithm (only Viterbi for the moment) + int beam_size = 500 // Size of the beam in Beam Search algorithm + ); protected: Ptr classifier; diff --git a/modules/text/src/ocr_beamsearch_decoder.cpp b/modules/text/src/ocr_beamsearch_decoder.cpp index 4b0c43c0fac..6f34056ed90 100644 --- a/modules/text/src/ocr_beamsearch_decoder.cpp +++ b/modules/text/src/ocr_beamsearch_decoder.cpp @@ -499,21 +499,11 @@ Ptr OCRBeamSearchDecoder::create( Ptr(_classifier, _vocabulary, transition_p, emission_p, _mode, _beam_size); } -Ptr OCRBeamSearchDecoder::create(Ptr _classifier, - const String& _vocabulary, - InputArray transition_p, - InputArray emission_p, - int _mode, - int _beam_size) -{ - return makePtr(_classifier, _vocabulary, transition_p, emission_p, (decoder_mode)_mode, _beam_size); -} - Ptr OCRBeamSearchDecoder::create(const String& _filename, const String& _vocabulary, InputArray transition_p, InputArray emission_p, - int _mode, + decoder_mode _mode, int _beam_size) { return makePtr(loadOCRBeamSearchClassifierCNN(_filename), _vocabulary, transition_p, emission_p, (decoder_mode)_mode, _beam_size); From 2cb3f6513185b3a7e7dd1a8c187359f6adb8abb1 Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Tue, 27 Oct 2020 18:05:44 +0000 Subject: [PATCH 19/29] tracking: move/copy files before modification --- modules/tracking/include/opencv2/tracking.hpp | 284 --- .../{tracker.hpp => tracking_internals.hpp} | 0 .../opencv2/tracking/tracking_legacy.hpp | 1548 +++++++++++++++++ .../tracker.legacy.hpp} | 0 .../trackerCSRT.legacy.hpp} | 0 .../trackerKCF.legacy.hpp} | 0 .../trackerMIL.legacy.hpp} | 0 7 files changed, 1548 insertions(+), 284 deletions(-) delete mode 100644 modules/tracking/include/opencv2/tracking.hpp rename modules/tracking/include/opencv2/tracking/{tracker.hpp => tracking_internals.hpp} (100%) create mode 100644 modules/tracking/include/opencv2/tracking/tracking_legacy.hpp rename modules/tracking/src/{tracker.cpp => legacy/tracker.legacy.hpp} (100%) rename modules/tracking/src/{trackerCSRT.cpp => legacy/trackerCSRT.legacy.hpp} (100%) rename modules/tracking/src/{trackerKCF.cpp => legacy/trackerKCF.legacy.hpp} (100%) rename modules/tracking/src/{trackerMIL.cpp => legacy/trackerMIL.legacy.hpp} (100%) diff --git a/modules/tracking/include/opencv2/tracking.hpp b/modules/tracking/include/opencv2/tracking.hpp deleted file mode 100644 index 516f5b979bc..00000000000 --- a/modules/tracking/include/opencv2/tracking.hpp +++ /dev/null @@ -1,284 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#ifndef __OPENCV_TRACKING_HPP__ -#define __OPENCV_TRACKING_HPP__ - -#include "opencv2/core/cvdef.h" - -/** @defgroup tracking Tracking API - -Long-term optical tracking API ------------------------------- - -Long-term optical tracking is an important issue for many computer vision applications in -real world scenario. The development in this area is very fragmented and this API is an unique -interface useful for plug several algorithms and compare them. This work is partially based on -@cite AAM and @cite AMVOT . - -These algorithms start from a bounding box of the target and with their internal representation they -avoid the drift during the tracking. These long-term trackers are able to evaluate online the -quality of the location of the target in the new frame, without ground truth. - -There are three main components: the TrackerSampler, the TrackerFeatureSet and the TrackerModel. The -first component is the object that computes the patches over the frame based on the last target -location. The TrackerFeatureSet is the class that manages the Features, is possible plug many kind -of these (HAAR, HOG, LBP, Feature2D, etc). The last component is the internal representation of the -target, it is the appearance model. It stores all state candidates and compute the trajectory (the -most likely target states). The class TrackerTargetState represents a possible state of the target. -The TrackerSampler and the TrackerFeatureSet are the visual representation of the target, instead -the TrackerModel is the statistical model. - -A recent benchmark between these algorithms can be found in @cite OOT - -Creating Your Own %Tracker --------------------- - -If you want to create a new tracker, here's what you have to do. First, decide on the name of the class -for the tracker (to meet the existing style, we suggest something with prefix "tracker", e.g. -trackerMIL, trackerBoosting) -- we shall refer to this choice as to "classname" in subsequent. - -- Declare your tracker in modules/tracking/include/opencv2/tracking/tracker.hpp. Your tracker should inherit from - Tracker (please, see the example below). You should declare the specialized Param structure, - where you probably will want to put the data, needed to initialize your tracker. You should - get something similar to : -@code - class CV_EXPORTS_W TrackerMIL : public Tracker - { - public: - struct CV_EXPORTS Params - { - Params(); - //parameters for sampler - float samplerInitInRadius; // radius for gathering positive instances during init - int samplerInitMaxNegNum; // # negative samples to use during init - float samplerSearchWinSize; // size of search window - float samplerTrackInRadius; // radius for gathering positive instances during tracking - int samplerTrackMaxPosNum; // # positive samples to use during tracking - int samplerTrackMaxNegNum; // # negative samples to use during tracking - int featureSetNumFeatures; // #features - - void read( const FileNode& fn ); - void write( FileStorage& fs ) const; - }; -@endcode - of course, you can also add any additional methods of your choice. It should be pointed out, - however, that it is not expected to have a constructor declared, as creation should be done via - the corresponding create() method. -- Finally, you should implement the function with signature : -@code - Ptr classname::create(const classname::Params ¶meters){ - ... - } -@endcode - That function can (and probably will) return a pointer to some derived class of "classname", - which will probably have a real constructor. - -Every tracker has three component TrackerSampler, TrackerFeatureSet and TrackerModel. The first two -are instantiated from Tracker base class, instead the last component is abstract, so you must -implement your TrackerModel. - -### TrackerSampler - -TrackerSampler is already instantiated, but you should define the sampling algorithm and add the -classes (or single class) to TrackerSampler. You can choose one of the ready implementation as -TrackerSamplerCSC or you can implement your sampling method, in this case the class must inherit -TrackerSamplerAlgorithm. Fill the samplingImpl method that writes the result in "sample" output -argument. - -Example of creating specialized TrackerSamplerAlgorithm TrackerSamplerCSC : : -@code - class CV_EXPORTS_W TrackerSamplerCSC : public TrackerSamplerAlgorithm - { - public: - TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meters = TrackerSamplerCSC::Params() ); - ~TrackerSamplerCSC(); - ... - - protected: - bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ); - ... - - }; -@endcode - -Example of adding TrackerSamplerAlgorithm to TrackerSampler : : -@code - //sampler is the TrackerSampler - Ptr CSCSampler = new TrackerSamplerCSC( CSCparameters ); - if( !sampler->addTrackerSamplerAlgorithm( CSCSampler ) ) - return false; - - //or add CSC sampler with default parameters - //sampler->addTrackerSamplerAlgorithm( "CSC" ); -@endcode -@sa - TrackerSamplerCSC, TrackerSamplerAlgorithm - -### TrackerFeatureSet - -TrackerFeatureSet is already instantiated (as first) , but you should define what kinds of features -you'll use in your tracker. You can use multiple feature types, so you can add a ready -implementation as TrackerFeatureHAAR in your TrackerFeatureSet or develop your own implementation. -In this case, in the computeImpl method put the code that extract the features and in the selection -method optionally put the code for the refinement and selection of the features. - -Example of creating specialized TrackerFeature TrackerFeatureHAAR : : -@code - class CV_EXPORTS_W TrackerFeatureHAAR : public TrackerFeature - { - public: - TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶meters = TrackerFeatureHAAR::Params() ); - ~TrackerFeatureHAAR(); - void selection( Mat& response, int npoints ); - ... - - protected: - bool computeImpl( const std::vector& images, Mat& response ); - ... - - }; -@endcode -Example of adding TrackerFeature to TrackerFeatureSet : : -@code - //featureSet is the TrackerFeatureSet - Ptr trackerFeature = new TrackerFeatureHAAR( HAARparameters ); - featureSet->addTrackerFeature( trackerFeature ); -@endcode -@sa - TrackerFeatureHAAR, TrackerFeatureSet - -### TrackerModel - -TrackerModel is abstract, so in your implementation you must develop your TrackerModel that inherit -from TrackerModel. Fill the method for the estimation of the state "modelEstimationImpl", that -estimates the most likely target location, see @cite AAM table I (ME) for further information. Fill -"modelUpdateImpl" in order to update the model, see @cite AAM table I (MU). In this class you can use -the :cConfidenceMap and :cTrajectory to storing the model. The first represents the model on the all -possible candidate states and the second represents the list of all estimated states. - -Example of creating specialized TrackerModel TrackerMILModel : : -@code - class TrackerMILModel : public TrackerModel - { - public: - TrackerMILModel( const Rect& boundingBox ); - ~TrackerMILModel(); - ... - - protected: - void modelEstimationImpl( const std::vector& responses ); - void modelUpdateImpl(); - ... - - }; -@endcode -And add it in your Tracker : : -@code - bool TrackerMIL::initImpl( const Mat& image, const Rect2d& boundingBox ) - { - ... - //model is the general TrackerModel field of the general Tracker - model = new TrackerMILModel( boundingBox ); - ... - } -@endcode -In the last step you should define the TrackerStateEstimator based on your implementation or you can -use one of ready class as TrackerStateEstimatorMILBoosting. It represent the statistical part of the -model that estimates the most likely target state. - -Example of creating specialized TrackerStateEstimator TrackerStateEstimatorMILBoosting : : -@code - class CV_EXPORTS_W TrackerStateEstimatorMILBoosting : public TrackerStateEstimator - { - class TrackerMILTargetState : public TrackerTargetState - { - ... - }; - - public: - TrackerStateEstimatorMILBoosting( int nFeatures = 250 ); - ~TrackerStateEstimatorMILBoosting(); - ... - - protected: - Ptr estimateImpl( const std::vector& confidenceMaps ); - void updateImpl( std::vector& confidenceMaps ); - ... - - }; -@endcode -And add it in your TrackerModel : : -@code - //model is the TrackerModel of your Tracker - Ptr stateEstimator = new TrackerStateEstimatorMILBoosting( params.featureSetNumFeatures ); - model->setTrackerStateEstimator( stateEstimator ); -@endcode -@sa - TrackerModel, TrackerStateEstimatorMILBoosting, TrackerTargetState - -During this step, you should define your TrackerTargetState based on your implementation. -TrackerTargetState base class has only the bounding box (upper-left position, width and height), you -can enrich it adding scale factor, target rotation, etc. - -Example of creating specialized TrackerTargetState TrackerMILTargetState : : -@code - class TrackerMILTargetState : public TrackerTargetState - { - public: - TrackerMILTargetState( const Point2f& position, int targetWidth, int targetHeight, bool foreground, const Mat& features ); - ~TrackerMILTargetState(); - ... - - private: - bool isTarget; - Mat targetFeatures; - ... - - }; -@endcode - -*/ - -#include -#include - -#endif //__OPENCV_TRACKING_HPP__ diff --git a/modules/tracking/include/opencv2/tracking/tracker.hpp b/modules/tracking/include/opencv2/tracking/tracking_internals.hpp similarity index 100% rename from modules/tracking/include/opencv2/tracking/tracker.hpp rename to modules/tracking/include/opencv2/tracking/tracking_internals.hpp diff --git a/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp b/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp new file mode 100644 index 00000000000..3c09c2b771b --- /dev/null +++ b/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp @@ -0,0 +1,1548 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// + // + // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. + // + // By downloading, copying, installing or using the software you agree to this license. + // If you do not agree to this license, do not download, install, + // copy or use the software. + // + // + // License Agreement + // For Open Source Computer Vision Library + // + // Copyright (C) 2013, OpenCV Foundation, all rights reserved. + // Third party copyrights are property of their respective owners. + // + // Redistribution and use in source and binary forms, with or without modification, + // are permitted provided that the following conditions are met: + // + // * Redistribution's of source code must retain the above copyright notice, + // this list of conditions and the following disclaimer. + // + // * Redistribution's in binary form must reproduce the above copyright notice, + // this list of conditions and the following disclaimer in the documentation + // and/or other materials provided with the distribution. + // + // * The name of the copyright holders may not be used to endorse or promote products + // derived from this software without specific prior written permission. + // + // This software is provided by the copyright holders and contributors "as is" and + // any express or implied warranties, including, but not limited to, the implied + // warranties of merchantability and fitness for a particular purpose are disclaimed. + // In no event shall the Intel Corporation or contributors be liable for any direct, + // indirect, incidental, special, exemplary, or consequential damages + // (including, but not limited to, procurement of substitute goods or services; + // loss of use, data, or profits; or business interruption) however caused + // and on any theory of liability, whether in contract, strict liability, + // or tort (including negligence or otherwise) arising in any way out of + // the use of this software, even if advised of the possibility of such damage. + // + //M*/ + +#ifndef __OPENCV_TRACKER_HPP__ +#define __OPENCV_TRACKER_HPP__ + +#include "opencv2/core.hpp" +#include "opencv2/imgproc/types_c.h" +#include "feature.hpp" +#include "onlineMIL.hpp" +#include "onlineBoosting.hpp" + +/* + * Partially based on: + * ==================================================================================================================== + * - [AAM] S. Salti, A. Cavallaro, L. Di Stefano, Adaptive Appearance Modeling for Video Tracking: Survey and Evaluation + * - [AMVOT] X. Li, W. Hu, C. Shen, Z. Zhang, A. Dick, A. van den Hengel, A Survey of Appearance Models in Visual Object Tracking + * + * This Tracking API has been designed with PlantUML. If you modify this API please change UML files under modules/tracking/doc/uml + * + */ + +namespace cv +{ + +//! @addtogroup tracking +//! @{ + +/************************************ TrackerFeature Base Classes ************************************/ + +/** @brief Abstract base class for TrackerFeature that represents the feature. + */ +class CV_EXPORTS TrackerFeature +{ + public: + virtual ~TrackerFeature(); + + /** @brief Compute the features in the images collection + @param images The images + @param response The output response + */ + void compute( const std::vector& images, Mat& response ); + + /** @brief Create TrackerFeature by tracker feature type + @param trackerFeatureType The TrackerFeature name + + The modes available now: + + - "HAAR" -- Haar Feature-based + + The modes that will be available soon: + + - "HOG" -- Histogram of Oriented Gradients features + - "LBP" -- Local Binary Pattern features + - "FEATURE2D" -- All types of Feature2D + */ + static Ptr create( const String& trackerFeatureType ); + + /** @brief Identify most effective features + @param response Collection of response for the specific TrackerFeature + @param npoints Max number of features + + @note This method modifies the response parameter + */ + virtual void selection( Mat& response, int npoints ) = 0; + + /** @brief Get the name of the specific TrackerFeature + */ + String getClassName() const; + + protected: + + virtual bool computeImpl( const std::vector& images, Mat& response ) = 0; + + String className; +}; + +/** @brief Class that manages the extraction and selection of features + +@cite AAM Feature Extraction and Feature Set Refinement (Feature Processing and Feature Selection). +See table I and section III C @cite AMVOT Appearance modelling -\> Visual representation (Table II, +section 3.1 - 3.2) + +TrackerFeatureSet is an aggregation of TrackerFeature + +@sa + TrackerFeature + + */ +class CV_EXPORTS TrackerFeatureSet +{ + public: + + TrackerFeatureSet(); + + ~TrackerFeatureSet(); + + /** @brief Extract features from the images collection + @param images The input images + */ + void extraction( const std::vector& images ); + + /** @brief Identify most effective features for all feature types (optional) + */ + void selection(); + + /** @brief Remove outliers for all feature types (optional) + */ + void removeOutliers(); + + /** @brief Add TrackerFeature in the collection. Return true if TrackerFeature is added, false otherwise + @param trackerFeatureType The TrackerFeature name + + The modes available now: + + - "HAAR" -- Haar Feature-based + + The modes that will be available soon: + + - "HOG" -- Histogram of Oriented Gradients features + - "LBP" -- Local Binary Pattern features + - "FEATURE2D" -- All types of Feature2D + + Example TrackerFeatureSet::addTrackerFeature : : + @code + //sample usage: + + Ptr trackerFeature = new TrackerFeatureHAAR( HAARparameters ); + featureSet->addTrackerFeature( trackerFeature ); + + //or add CSC sampler with default parameters + //featureSet->addTrackerFeature( "HAAR" ); + @endcode + @note If you use the second method, you must initialize the TrackerFeature + */ + bool addTrackerFeature( String trackerFeatureType ); + + /** @overload + @param feature The TrackerFeature class + */ + bool addTrackerFeature( Ptr& feature ); + + /** @brief Get the TrackerFeature collection (TrackerFeature name, TrackerFeature pointer) + */ + const std::vector > >& getTrackerFeature() const; + + /** @brief Get the responses + + @note Be sure to call extraction before getResponses Example TrackerFeatureSet::getResponses : : + */ + const std::vector& getResponses() const; + + private: + + void clearResponses(); + bool blockAddTrackerFeature; + + std::vector > > features; //list of features + std::vector responses; //list of response after compute + +}; + +/************************************ TrackerSampler Base Classes ************************************/ + +/** @brief Abstract base class for TrackerSamplerAlgorithm that represents the algorithm for the specific +sampler. + */ +class CV_EXPORTS TrackerSamplerAlgorithm +{ + public: + /** + * \brief Destructor + */ + virtual ~TrackerSamplerAlgorithm(); + + /** @brief Create TrackerSamplerAlgorithm by tracker sampler type. + @param trackerSamplerType The trackerSamplerType name + + The modes available now: + + - "CSC" -- Current State Center + - "CS" -- Current State + */ + static Ptr create( const String& trackerSamplerType ); + + /** @brief Computes the regions starting from a position in an image. + + Return true if samples are computed, false otherwise + + @param image The current frame + @param boundingBox The bounding box from which regions can be calculated + + @param sample The computed samples @cite AAM Fig. 1 variable Sk + */ + bool sampling( const Mat& image, Rect boundingBox, std::vector& sample ); + + /** @brief Get the name of the specific TrackerSamplerAlgorithm + */ + String getClassName() const; + + protected: + String className; + + virtual bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) = 0; +}; + +/** + * \brief Class that manages the sampler in order to select regions for the update the model of the tracker + * [AAM] Sampling e Labeling. See table I and section III B + */ + +/** @brief Class that manages the sampler in order to select regions for the update the model of the tracker + +@cite AAM Sampling e Labeling. See table I and section III B + +TrackerSampler is an aggregation of TrackerSamplerAlgorithm +@sa + TrackerSamplerAlgorithm + */ +class CV_EXPORTS TrackerSampler +{ + public: + + /** + * \brief Constructor + */ + TrackerSampler(); + + /** + * \brief Destructor + */ + ~TrackerSampler(); + + /** @brief Computes the regions starting from a position in an image + @param image The current frame + @param boundingBox The bounding box from which regions can be calculated + */ + void sampling( const Mat& image, Rect boundingBox ); + + /** @brief Return the collection of the TrackerSamplerAlgorithm + */ + const std::vector > >& getSamplers() const; + + /** @brief Return the samples from all TrackerSamplerAlgorithm, @cite AAM Fig. 1 variable Sk + */ + const std::vector& getSamples() const; + + /** @brief Add TrackerSamplerAlgorithm in the collection. Return true if sampler is added, false otherwise + @param trackerSamplerAlgorithmType The TrackerSamplerAlgorithm name + + The modes available now: + - "CSC" -- Current State Center + - "CS" -- Current State + - "PF" -- Particle Filtering + + Example TrackerSamplerAlgorithm::addTrackerSamplerAlgorithm : : + @code + TrackerSamplerCSC::Params CSCparameters; + Ptr CSCSampler = new TrackerSamplerCSC( CSCparameters ); + + if( !sampler->addTrackerSamplerAlgorithm( CSCSampler ) ) + return false; + + //or add CSC sampler with default parameters + //sampler->addTrackerSamplerAlgorithm( "CSC" ); + @endcode + @note If you use the second method, you must initialize the TrackerSamplerAlgorithm + */ + bool addTrackerSamplerAlgorithm( String trackerSamplerAlgorithmType ); + + /** @overload + @param sampler The TrackerSamplerAlgorithm + */ + bool addTrackerSamplerAlgorithm( Ptr& sampler ); + + private: + std::vector > > samplers; + std::vector samples; + bool blockAddTrackerSampler; + + void clearSamples(); +}; + +/************************************ TrackerModel Base Classes ************************************/ + +/** @brief Abstract base class for TrackerTargetState that represents a possible state of the target. + +See @cite AAM \f$\hat{x}^{i}_{k}\f$ all the states candidates. + +Inherits this class with your Target state, In own implementation you can add scale variation, +width, height, orientation, etc. + */ +class CV_EXPORTS TrackerTargetState +{ + public: + virtual ~TrackerTargetState() + { + } + ; + /** + * \brief Get the position + * \return The position + */ + Point2f getTargetPosition() const; + + /** + * \brief Set the position + * \param position The position + */ + void setTargetPosition( const Point2f& position ); + /** + * \brief Get the width of the target + * \return The width of the target + */ + int getTargetWidth() const; + + /** + * \brief Set the width of the target + * \param width The width of the target + */ + void setTargetWidth( int width ); + /** + * \brief Get the height of the target + * \return The height of the target + */ + int getTargetHeight() const; + + /** + * \brief Set the height of the target + * \param height The height of the target + */ + void setTargetHeight( int height ); + + protected: + Point2f targetPosition; + int targetWidth; + int targetHeight; + +}; + +/** @brief Represents the model of the target at frame \f$k\f$ (all states and scores) + +See @cite AAM The set of the pair \f$\langle \hat{x}^{i}_{k}, C^{i}_{k} \rangle\f$ +@sa TrackerTargetState + */ +typedef std::vector, float> > ConfidenceMap; + +/** @brief Represents the estimate states for all frames + +@cite AAM \f$x_{k}\f$ is the trajectory of the target up to time \f$k\f$ + +@sa TrackerTargetState + */ +typedef std::vector > Trajectory; + +/** @brief Abstract base class for TrackerStateEstimator that estimates the most likely target state. + +See @cite AAM State estimator + +See @cite AMVOT Statistical modeling (Fig. 3), Table III (generative) - IV (discriminative) - V (hybrid) + */ +class CV_EXPORTS TrackerStateEstimator +{ + public: + virtual ~TrackerStateEstimator(); + + /** @brief Estimate the most likely target state, return the estimated state + @param confidenceMaps The overall appearance model as a list of :cConfidenceMap + */ + Ptr estimate( const std::vector& confidenceMaps ); + + /** @brief Update the ConfidenceMap with the scores + @param confidenceMaps The overall appearance model as a list of :cConfidenceMap + */ + void update( std::vector& confidenceMaps ); + + /** @brief Create TrackerStateEstimator by tracker state estimator type + @param trackeStateEstimatorType The TrackerStateEstimator name + + The modes available now: + + - "BOOSTING" -- Boosting-based discriminative appearance models. See @cite AMVOT section 4.4 + + The modes available soon: + + - "SVM" -- SVM-based discriminative appearance models. See @cite AMVOT section 4.5 + */ + static Ptr create( const String& trackeStateEstimatorType ); + + /** @brief Get the name of the specific TrackerStateEstimator + */ + String getClassName() const; + + protected: + + virtual Ptr estimateImpl( const std::vector& confidenceMaps ) = 0; + virtual void updateImpl( std::vector& confidenceMaps ) = 0; + String className; +}; + +/** @brief Abstract class that represents the model of the target. It must be instantiated by specialized +tracker + +See @cite AAM Ak + +Inherits this with your TrackerModel + */ +class CV_EXPORTS TrackerModel +{ + public: + + /** + * \brief Constructor + */ + TrackerModel(); + + /** + * \brief Destructor + */ + virtual ~TrackerModel(); + + /** @brief Set TrackerEstimator, return true if the tracker state estimator is added, false otherwise + @param trackerStateEstimator The TrackerStateEstimator + @note You can add only one TrackerStateEstimator + */ + bool setTrackerStateEstimator( Ptr trackerStateEstimator ); + + /** @brief Estimate the most likely target location + + @cite AAM ME, Model Estimation table I + @param responses Features extracted from TrackerFeatureSet + */ + void modelEstimation( const std::vector& responses ); + + /** @brief Update the model + + @cite AAM MU, Model Update table I + */ + void modelUpdate(); + + /** @brief Run the TrackerStateEstimator, return true if is possible to estimate a new state, false otherwise + */ + bool runStateEstimator(); + + /** @brief Set the current TrackerTargetState in the Trajectory + @param lastTargetState The current TrackerTargetState + */ + void setLastTargetState( const Ptr& lastTargetState ); + + /** @brief Get the last TrackerTargetState from Trajectory + */ + Ptr getLastTargetState() const; + + /** @brief Get the list of the ConfidenceMap + */ + const std::vector& getConfidenceMaps() const; + + /** @brief Get the last ConfidenceMap for the current frame + */ + const ConfidenceMap& getLastConfidenceMap() const; + + /** @brief Get the TrackerStateEstimator + */ + Ptr getTrackerStateEstimator() const; + + private: + + void clearCurrentConfidenceMap(); + + protected: + std::vector confidenceMaps; + Ptr stateEstimator; + ConfidenceMap currentConfidenceMap; + Trajectory trajectory; + int maxCMLength; + + virtual void modelEstimationImpl( const std::vector& responses ) = 0; + virtual void modelUpdateImpl() = 0; + +}; + +/************************************ Tracker Base Class ************************************/ + +/** @brief Base abstract class for the long-term tracker: + */ +class CV_EXPORTS_W Tracker : public virtual Algorithm +{ + public: + + virtual ~Tracker() CV_OVERRIDE; + + /** @brief Initialize the tracker with a known bounding box that surrounded the target + @param image The initial frame + @param boundingBox The initial bounding box + + @return True if initialization went succesfully, false otherwise + */ + CV_WRAP bool init( InputArray image, const Rect2d& boundingBox ); + + /** @brief Update the tracker, find the new most likely bounding box for the target + @param image The current frame + @param boundingBox The bounding box that represent the new target location, if true was returned, not + modified otherwise + + @return True means that target was located and false means that tracker cannot locate target in + current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed + missing from the frame (say, out of sight) + */ + CV_WRAP bool update( InputArray image, CV_OUT Rect2d& boundingBox ); + + virtual void read( const FileNode& fn ) CV_OVERRIDE = 0; + virtual void write( FileStorage& fs ) const CV_OVERRIDE = 0; + + protected: + + virtual bool initImpl( const Mat& image, const Rect2d& boundingBox ) = 0; + virtual bool updateImpl( const Mat& image, Rect2d& boundingBox ) = 0; + + bool isInit; + + Ptr featureSet; + Ptr sampler; + Ptr model; +}; + + +/************************************ Specific TrackerStateEstimator Classes ************************************/ + +/** @brief TrackerStateEstimator based on Boosting + */ +class CV_EXPORTS TrackerStateEstimatorMILBoosting : public TrackerStateEstimator +{ + public: + + /** + * Implementation of the target state for TrackerStateEstimatorMILBoosting + */ + class TrackerMILTargetState : public TrackerTargetState + { + + public: + /** + * \brief Constructor + * \param position Top left corner of the bounding box + * \param width Width of the bounding box + * \param height Height of the bounding box + * \param foreground label for target or background + * \param features features extracted + */ + TrackerMILTargetState( const Point2f& position, int width, int height, bool foreground, const Mat& features ); + + /** + * \brief Destructor + */ + ~TrackerMILTargetState() + { + } + ; + + /** @brief Set label: true for target foreground, false for background + @param foreground Label for background/foreground + */ + void setTargetFg( bool foreground ); + /** @brief Set the features extracted from TrackerFeatureSet + @param features The features extracted + */ + void setFeatures( const Mat& features ); + /** @brief Get the label. Return true for target foreground, false for background + */ + bool isTargetFg() const; + /** @brief Get the features extracted + */ + Mat getFeatures() const; + + private: + bool isTarget; + Mat targetFeatures; + }; + + /** @brief Constructor + @param nFeatures Number of features for each sample + */ + TrackerStateEstimatorMILBoosting( int nFeatures = 250 ); + ~TrackerStateEstimatorMILBoosting(); + + /** @brief Set the current confidenceMap + @param confidenceMap The current :cConfidenceMap + */ + void setCurrentConfidenceMap( ConfidenceMap& confidenceMap ); + + protected: + Ptr estimateImpl( const std::vector& confidenceMaps ) CV_OVERRIDE; + void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; + + private: + uint max_idx( const std::vector &v ); + void prepareData( const ConfidenceMap& confidenceMap, Mat& positive, Mat& negative ); + + ClfMilBoost boostMILModel; + bool trained; + int numFeatures; + + ConfidenceMap currentConfidenceMap; +}; + +/** @brief TrackerStateEstimatorAdaBoosting based on ADA-Boosting + */ +class CV_EXPORTS TrackerStateEstimatorAdaBoosting : public TrackerStateEstimator +{ + public: + /** @brief Implementation of the target state for TrackerAdaBoostingTargetState + */ + class TrackerAdaBoostingTargetState : public TrackerTargetState + { + + public: + /** + * \brief Constructor + * \param position Top left corner of the bounding box + * \param width Width of the bounding box + * \param height Height of the bounding box + * \param foreground label for target or background + * \param responses list of features + */ + TrackerAdaBoostingTargetState( const Point2f& position, int width, int height, bool foreground, const Mat& responses ); + + /** + * \brief Destructor + */ + ~TrackerAdaBoostingTargetState() + { + } + ; + + /** @brief Set the features extracted from TrackerFeatureSet + @param responses The features extracted + */ + void setTargetResponses( const Mat& responses ); + /** @brief Set label: true for target foreground, false for background + @param foreground Label for background/foreground + */ + void setTargetFg( bool foreground ); + /** @brief Get the features extracted + */ + Mat getTargetResponses() const; + /** @brief Get the label. Return true for target foreground, false for background + */ + bool isTargetFg() const; + + private: + bool isTarget; + Mat targetResponses; + + }; + + /** @brief Constructor + @param numClassifer Number of base classifiers + @param initIterations Number of iterations in the initialization + @param nFeatures Number of features/weak classifiers + @param patchSize tracking rect + @param ROI initial ROI + */ + TrackerStateEstimatorAdaBoosting( int numClassifer, int initIterations, int nFeatures, Size patchSize, const Rect& ROI ); + + /** + * \brief Destructor + */ + ~TrackerStateEstimatorAdaBoosting(); + + /** @brief Get the sampling ROI + */ + Rect getSampleROI() const; + + /** @brief Set the sampling ROI + @param ROI the sampling ROI + */ + void setSampleROI( const Rect& ROI ); + + /** @brief Set the current confidenceMap + @param confidenceMap The current :cConfidenceMap + */ + void setCurrentConfidenceMap( ConfidenceMap& confidenceMap ); + + /** @brief Get the list of the selected weak classifiers for the classification step + */ + std::vector computeSelectedWeakClassifier(); + + /** @brief Get the list of the weak classifiers that should be replaced + */ + std::vector computeReplacedClassifier(); + + /** @brief Get the list of the weak classifiers that replace those to be replaced + */ + std::vector computeSwappedClassifier(); + + protected: + Ptr estimateImpl( const std::vector& confidenceMaps ) CV_OVERRIDE; + void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; + + Ptr boostClassifier; + + private: + int numBaseClassifier; + int iterationInit; + int numFeatures; + bool trained; + Size initPatchSize; + Rect sampleROI; + std::vector replacedClassifier; + std::vector swappedClassifier; + + ConfidenceMap currentConfidenceMap; +}; + +/** + * \brief TrackerStateEstimator based on SVM + */ +class CV_EXPORTS TrackerStateEstimatorSVM : public TrackerStateEstimator +{ + public: + TrackerStateEstimatorSVM(); + ~TrackerStateEstimatorSVM(); + + protected: + Ptr estimateImpl( const std::vector& confidenceMaps ) CV_OVERRIDE; + void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; +}; + +/************************************ Specific TrackerSamplerAlgorithm Classes ************************************/ + +/** @brief TrackerSampler based on CSC (current state centered), used by MIL algorithm TrackerMIL + */ +class CV_EXPORTS TrackerSamplerCSC : public TrackerSamplerAlgorithm +{ + public: + enum + { + MODE_INIT_POS = 1, //!< mode for init positive samples + MODE_INIT_NEG = 2, //!< mode for init negative samples + MODE_TRACK_POS = 3, //!< mode for update positive samples + MODE_TRACK_NEG = 4, //!< mode for update negative samples + MODE_DETECT = 5 //!< mode for detect samples + }; + + struct CV_EXPORTS Params + { + Params(); + float initInRad; //!< radius for gathering positive instances during init + float trackInPosRad; //!< radius for gathering positive instances during tracking + float searchWinSize; //!< size of search window + int initMaxNegNum; //!< # negative samples to use during init + int trackMaxPosNum; //!< # positive samples to use during training + int trackMaxNegNum; //!< # negative samples to use during training + }; + + /** @brief Constructor + @param parameters TrackerSamplerCSC parameters TrackerSamplerCSC::Params + */ + TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meters = TrackerSamplerCSC::Params() ); + + /** @brief Set the sampling mode of TrackerSamplerCSC + @param samplingMode The sampling mode + + The modes are: + + - "MODE_INIT_POS = 1" -- for the positive sampling in initialization step + - "MODE_INIT_NEG = 2" -- for the negative sampling in initialization step + - "MODE_TRACK_POS = 3" -- for the positive sampling in update step + - "MODE_TRACK_NEG = 4" -- for the negative sampling in update step + - "MODE_DETECT = 5" -- for the sampling in detection step + */ + void setMode( int samplingMode ); + + ~TrackerSamplerCSC(); + + protected: + + bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) CV_OVERRIDE; + + private: + + Params params; + int mode; + RNG rng; + + std::vector sampleImage( const Mat& img, int x, int y, int w, int h, float inrad, float outrad = 0, int maxnum = 1000000 ); +}; + +/** @brief TrackerSampler based on CS (current state), used by algorithm TrackerBoosting + */ +class CV_EXPORTS TrackerSamplerCS : public TrackerSamplerAlgorithm +{ + public: + enum + { + MODE_POSITIVE = 1, //!< mode for positive samples + MODE_NEGATIVE = 2, //!< mode for negative samples + MODE_CLASSIFY = 3 //!< mode for classify samples + }; + + struct CV_EXPORTS Params + { + Params(); + float overlap; //!& sample ) CV_OVERRIDE; + Rect getROI() const; + private: + Rect getTrackingROI( float searchFactor ); + Rect RectMultiply( const Rect & rect, float f ); + std::vector patchesRegularScan( const Mat& image, Rect trackingROI, Size patchSize ); + void setCheckedROI( Rect imageROI ); + + Params params; + int mode; + Rect trackedPatch; + Rect validROI; + Rect ROI; + +}; + +/** @brief This sampler is based on particle filtering. + +In principle, it can be thought of as performing some sort of optimization (and indeed, this +tracker uses opencv's optim module), where tracker seeks to find the rectangle in given frame, +which is the most *"similar"* to the initial rectangle (the one, given through the constructor). + +The optimization performed is stochastic and somehow resembles genetic algorithms, where on each new +image received (submitted via TrackerSamplerPF::sampling()) we start with the region bounded by +boundingBox, then generate several "perturbed" boxes, take the ones most similar to the original. +This selection round is repeated several times. At the end, we hope that only the most promising box +remaining, and these are combined to produce the subrectangle of image, which is put as a sole +element in array sample. + +It should be noted, that the definition of "similarity" between two rectangles is based on comparing +their histograms. As experiments show, tracker is *not* very succesfull if target is assumed to +strongly change its dimensions. + */ +class CV_EXPORTS TrackerSamplerPF : public TrackerSamplerAlgorithm +{ +public: + /** @brief This structure contains all the parameters that can be varied during the course of sampling + algorithm. Below is the structure exposed, together with its members briefly explained with + reference to the above discussion on algorithm's working. + */ + struct CV_EXPORTS Params + { + Params(); + int iterationNum; //!< number of selection rounds + int particlesNum; //!< number of "perturbed" boxes on each round + double alpha; //!< with each new round we exponentially decrease the amount of "perturbing" we allow (like in simulated annealing) + //!< and this very alpha controls how fast annealing happens, ie. how fast perturbing decreases + Mat_ std; //!< initial values for perturbing (1-by-4 array, as each rectangle is given by 4 values -- coordinates of opposite vertices, + //!< hence we have 4 values to perturb) + }; + /** @brief Constructor + @param chosenRect Initial rectangle, that is supposed to contain target we'd like to track. + @param parameters + */ + TrackerSamplerPF(const Mat& chosenRect,const TrackerSamplerPF::Params ¶meters = TrackerSamplerPF::Params()); +protected: + bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) CV_OVERRIDE; +private: + Params params; + Ptr _solver; + Ptr _function; +}; + +/************************************ Specific TrackerFeature Classes ************************************/ + +/** + * \brief TrackerFeature based on Feature2D + */ +class CV_EXPORTS TrackerFeatureFeature2d : public TrackerFeature +{ + public: + + /** + * \brief Constructor + * \param detectorType string of FeatureDetector + * \param descriptorType string of DescriptorExtractor + */ + TrackerFeatureFeature2d( String detectorType, String descriptorType ); + + ~TrackerFeatureFeature2d() CV_OVERRIDE; + + void selection( Mat& response, int npoints ) CV_OVERRIDE; + + protected: + + bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; + + private: + + std::vector keypoints; +}; + +/** + * \brief TrackerFeature based on HOG + */ +class CV_EXPORTS TrackerFeatureHOG : public TrackerFeature +{ + public: + + TrackerFeatureHOG(); + + ~TrackerFeatureHOG() CV_OVERRIDE; + + void selection( Mat& response, int npoints ) CV_OVERRIDE; + + protected: + + bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; + +}; + +/** @brief TrackerFeature based on HAAR features, used by TrackerMIL and many others algorithms +@note HAAR features implementation is copied from apps/traincascade and modified according to MIL + */ +class CV_EXPORTS TrackerFeatureHAAR : public TrackerFeature +{ + public: + struct CV_EXPORTS Params + { + Params(); + int numFeatures; //!< # of rects + Size rectSize; //!< rect size + bool isIntegral; //!< true if input images are integral, false otherwise + }; + + /** @brief Constructor + @param parameters TrackerFeatureHAAR parameters TrackerFeatureHAAR::Params + */ + TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶meters = TrackerFeatureHAAR::Params() ); + + ~TrackerFeatureHAAR() CV_OVERRIDE; + + /** @brief Compute the features only for the selected indices in the images collection + @param selFeatures indices of selected features + @param images The images + @param response Collection of response for the specific TrackerFeature + */ + bool extractSelected( const std::vector selFeatures, const std::vector& images, Mat& response ); + + /** @brief Identify most effective features + @param response Collection of response for the specific TrackerFeature + @param npoints Max number of features + + @note This method modifies the response parameter + */ + void selection( Mat& response, int npoints ) CV_OVERRIDE; + + /** @brief Swap the feature in position source with the feature in position target + @param source The source position + @param target The target position + */ + bool swapFeature( int source, int target ); + + /** @brief Swap the feature in position id with the feature input + @param id The position + @param feature The feature + */ + bool swapFeature( int id, CvHaarEvaluator::FeatureHaar& feature ); + + /** @brief Get the feature in position id + @param id The position + */ + CvHaarEvaluator::FeatureHaar& getFeatureAt( int id ); + + protected: + bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; + + private: + + Params params; + Ptr featureEvaluator; +}; + +/** + * \brief TrackerFeature based on LBP + */ +class CV_EXPORTS TrackerFeatureLBP : public TrackerFeature +{ + public: + + TrackerFeatureLBP(); + + ~TrackerFeatureLBP(); + + void selection( Mat& response, int npoints ) CV_OVERRIDE; + + protected: + + bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; + +}; + +/************************************ Specific Tracker Classes ************************************/ + +/** @brief The MIL algorithm trains a classifier in an online manner to separate the object from the +background. + +Multiple Instance Learning avoids the drift problem for a robust tracking. The implementation is +based on @cite MIL . + +Original code can be found here + */ +class CV_EXPORTS_W TrackerMIL : public Tracker +{ + public: + struct CV_EXPORTS Params + { + Params(); + //parameters for sampler + float samplerInitInRadius; //!< radius for gathering positive instances during init + int samplerInitMaxNegNum; //!< # negative samples to use during init + float samplerSearchWinSize; //!< size of search window + float samplerTrackInRadius; //!< radius for gathering positive instances during tracking + int samplerTrackMaxPosNum; //!< # positive samples to use during tracking + int samplerTrackMaxNegNum; //!< # negative samples to use during tracking + int featureSetNumFeatures; //!< # features + + void read( const FileNode& fn ); + void write( FileStorage& fs ) const; + }; + + /** @brief Constructor + @param parameters MIL parameters TrackerMIL::Params + */ + static Ptr create(const TrackerMIL::Params ¶meters); + + CV_WRAP static Ptr create(); + + virtual ~TrackerMIL() CV_OVERRIDE {} +}; + +/** @brief the Boosting tracker + +This is a real-time object tracking based on a novel on-line version of the AdaBoost algorithm. +The classifier uses the surrounding background as negative examples in update step to avoid the +drifting problem. The implementation is based on @cite OLB . + */ +class CV_EXPORTS_W TrackerBoosting : public Tracker +{ + public: + struct CV_EXPORTS Params + { + Params(); + int numClassifiers; //! create(const TrackerBoosting::Params ¶meters); + + CV_WRAP static Ptr create(); + + virtual ~TrackerBoosting() CV_OVERRIDE {} +}; + +/** @brief the Median Flow tracker + +Implementation of a paper @cite MedianFlow . + +The tracker is suitable for very smooth and predictable movements when object is visible throughout +the whole sequence. It's quite and accurate for this type of problems (in particular, it was shown +by authors to outperform MIL). During the implementation period the code at +, the courtesy of the author Arthur Amarra, was used for the +reference purpose. + */ +class CV_EXPORTS_W TrackerMedianFlow : public Tracker +{ + public: + struct CV_EXPORTS Params + { + Params(); //! create(const TrackerMedianFlow::Params ¶meters); + + CV_WRAP static Ptr create(); + + virtual ~TrackerMedianFlow() CV_OVERRIDE {} +}; + +/** @brief the TLD (Tracking, learning and detection) tracker + +TLD is a novel tracking framework that explicitly decomposes the long-term tracking task into +tracking, learning and detection. + +The tracker follows the object from frame to frame. The detector localizes all appearances that +have been observed so far and corrects the tracker if necessary. The learning estimates detector's +errors and updates it to avoid these errors in the future. The implementation is based on @cite TLD . + +The Median Flow algorithm (see cv::TrackerMedianFlow) was chosen as a tracking component in this +implementation, following authors. The tracker is supposed to be able to handle rapid motions, partial +occlusions, object absence etc. + */ +class CV_EXPORTS_W TrackerTLD : public Tracker +{ + public: + struct CV_EXPORTS Params + { + Params(); + void read( const FileNode& /*fn*/ ); + void write( FileStorage& /*fs*/ ) const; + }; + + /** @brief Constructor + @param parameters TLD parameters TrackerTLD::Params + */ + static Ptr create(const TrackerTLD::Params ¶meters); + + CV_WRAP static Ptr create(); + + virtual ~TrackerTLD() CV_OVERRIDE {} +}; + +/** @brief the KCF (Kernelized Correlation Filter) tracker + + * KCF is a novel tracking framework that utilizes properties of circulant matrix to enhance the processing speed. + * This tracking method is an implementation of @cite KCF_ECCV which is extended to KCF with color-names features (@cite KCF_CN). + * The original paper of KCF is available at + * as well as the matlab implementation. For more information about KCF with color-names features, please refer to + * . + */ +class CV_EXPORTS_W TrackerKCF : public Tracker +{ +public: + /** + * \brief Feature type to be used in the tracking grayscale, colornames, compressed color-names + * The modes available now: + - "GRAY" -- Use grayscale values as the feature + - "CN" -- Color-names feature + */ + enum MODE { + GRAY = (1 << 0), + CN = (1 << 1), + CUSTOM = (1 << 2) + }; + + struct CV_EXPORTS Params + { + /** + * \brief Constructor + */ + Params(); + + /** + * \brief Read parameters from a file + */ + void read(const FileNode& /*fn*/); + + /** + * \brief Write parameters to a file + */ + void write(FileStorage& /*fs*/) const; + + float detect_thresh; //!< detection confidence threshold + float sigma; //!< gaussian kernel bandwidth + float lambda; //!< regularization + float interp_factor; //!< linear interpolation factor for adaptation + float output_sigma_factor; //!< spatial bandwidth (proportional to target) + float pca_learning_rate; //!< compression learning rate + bool resize; //!< activate the resize feature to improve the processing speed + bool split_coeff; //!< split the training coefficients into two matrices + bool wrap_kernel; //!< wrap around the kernel values + bool compress_feature; //!< activate the pca method to compress the features + int max_patch_size; //!< threshold for the ROI size + int compressed_size; //!< feature size after compression + int desc_pca; //!< compressed descriptors of TrackerKCF::MODE + int desc_npca; //!< non-compressed descriptors of TrackerKCF::MODE + }; + + virtual void setFeatureExtractor(void(*)(const Mat, const Rect, Mat&), bool pca_func = false) = 0; + + /** @brief Constructor + @param parameters KCF parameters TrackerKCF::Params + */ + static Ptr create(const TrackerKCF::Params ¶meters); + + CV_WRAP static Ptr create(); + + virtual ~TrackerKCF() CV_OVERRIDE {} +}; + +/** @brief the GOTURN (Generic Object Tracking Using Regression Networks) tracker + + * GOTURN (@cite GOTURN) is kind of trackers based on Convolutional Neural Networks (CNN). While taking all advantages of CNN trackers, + * GOTURN is much faster due to offline training without online fine-tuning nature. + * GOTURN tracker addresses the problem of single target tracking: given a bounding box label of an object in the first frame of the video, + * we track that object through the rest of the video. NOTE: Current method of GOTURN does not handle occlusions; however, it is fairly + * robust to viewpoint changes, lighting changes, and deformations. + * Inputs of GOTURN are two RGB patches representing Target and Search patches resized to 227x227. + * Outputs of GOTURN are predicted bounding box coordinates, relative to Search patch coordinate system, in format X1,Y1,X2,Y2. + * Original paper is here: + * As long as original authors implementation: + * Implementation of training algorithm is placed in separately here due to 3d-party dependencies: + * + * GOTURN architecture goturn.prototxt and trained model goturn.caffemodel are accessible on opencv_extra GitHub repository. +*/ +class CV_EXPORTS_W TrackerGOTURN : public Tracker +{ +public: + struct CV_EXPORTS Params + { + Params(); + void read(const FileNode& /*fn*/); + void write(FileStorage& /*fs*/) const; + String modelTxt; + String modelBin; + }; + + /** @brief Constructor + @param parameters GOTURN parameters TrackerGOTURN::Params + */ + static Ptr create(const TrackerGOTURN::Params ¶meters); + + CV_WRAP static Ptr create(); + + virtual ~TrackerGOTURN() CV_OVERRIDE {} +}; + +/** @brief the MOSSE (Minimum Output Sum of Squared %Error) tracker + +The implementation is based on @cite MOSSE Visual Object Tracking using Adaptive Correlation Filters +@note this tracker works with grayscale images, if passed bgr ones, they will get converted internally. +*/ + +class CV_EXPORTS_W TrackerMOSSE : public Tracker +{ + public: + /** @brief Constructor + */ + CV_WRAP static Ptr create(); + + virtual ~TrackerMOSSE() CV_OVERRIDE {} +}; + + +/************************************ MultiTracker Class ---By Laksono Kurnianggoro---) ************************************/ +/** @brief This class is used to track multiple objects using the specified tracker algorithm. + +* The %MultiTracker is naive implementation of multiple object tracking. +* It process the tracked objects independently without any optimization accross the tracked objects. +*/ +class CV_EXPORTS_W MultiTracker : public Algorithm +{ +public: + + /** + * \brief Constructor. + */ + CV_WRAP MultiTracker(); + + /** + * \brief Destructor + */ + ~MultiTracker() CV_OVERRIDE; + + /** + * \brief Add a new object to be tracked. + * + * @param newTracker tracking algorithm to be used + * @param image input image + * @param boundingBox a rectangle represents ROI of the tracked object + */ + CV_WRAP bool add(Ptr newTracker, InputArray image, const Rect2d& boundingBox); + + /** + * \brief Add a set of objects to be tracked. + * @param newTrackers list of tracking algorithms to be used + * @param image input image + * @param boundingBox list of the tracked objects + */ + bool add(std::vector > newTrackers, InputArray image, std::vector boundingBox); + + /** + * \brief Update the current tracking status. + * The result will be saved in the internal storage. + * @param image input image + */ + bool update(InputArray image); + + /** + * \brief Update the current tracking status. + * @param image input image + * @param boundingBox the tracking result, represent a list of ROIs of the tracked objects. + */ + CV_WRAP bool update(InputArray image, CV_OUT std::vector & boundingBox); + + /** + * \brief Returns a reference to a storage for the tracked objects, each object corresponds to one tracker algorithm + */ + CV_WRAP const std::vector& getObjects() const; + + /** + * \brief Returns a pointer to a new instance of MultiTracker + */ + CV_WRAP static Ptr create(); + +protected: + //!< storage for the tracker algorithms. + std::vector< Ptr > trackerList; + + //!< storage for the tracked objects, each object corresponds to one tracker algorithm. + std::vector objects; +}; + +/************************************ Multi-Tracker Classes ---By Tyan Vladimir---************************************/ + +/** @brief Base abstract class for the long-term Multi Object Trackers: + +@sa Tracker, MultiTrackerTLD +*/ +class CV_EXPORTS MultiTracker_Alt +{ +public: + /** @brief Constructor for Multitracker + */ + MultiTracker_Alt() + { + targetNum = 0; + } + + /** @brief Add a new target to a tracking-list and initialize the tracker with a known bounding box that surrounded the target + @param image The initial frame + @param boundingBox The initial bounding box of target + @param tracker_algorithm Multi-tracker algorithm + + @return True if new target initialization went succesfully, false otherwise + */ + bool addTarget(InputArray image, const Rect2d& boundingBox, Ptr tracker_algorithm); + + /** @brief Update all trackers from the tracking-list, find a new most likely bounding boxes for the targets + @param image The current frame + + @return True means that all targets were located and false means that tracker couldn't locate one of the targets in + current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed + missing from the frame (say, out of sight) + */ + bool update(InputArray image); + + /** @brief Current number of targets in tracking-list + */ + int targetNum; + + /** @brief Trackers list for Multi-Object-Tracker + */ + std::vector > trackers; + + /** @brief Bounding Boxes list for Multi-Object-Tracker + */ + std::vector boundingBoxes; + /** @brief List of randomly generated colors for bounding boxes display + */ + std::vector colors; +}; + +/** @brief Multi Object %Tracker for TLD. + +TLD is a novel tracking framework that explicitly decomposes +the long-term tracking task into tracking, learning and detection. + +The tracker follows the object from frame to frame. The detector localizes all appearances that +have been observed so far and corrects the tracker if necessary. The learning estimates detector's +errors and updates it to avoid these errors in the future. The implementation is based on @cite TLD . + +The Median Flow algorithm (see cv::TrackerMedianFlow) was chosen as a tracking component in this +implementation, following authors. The tracker is supposed to be able to handle rapid motions, partial +occlusions, object absence etc. + +@sa Tracker, MultiTracker, TrackerTLD +*/ +class CV_EXPORTS MultiTrackerTLD : public MultiTracker_Alt +{ +public: + /** @brief Update all trackers from the tracking-list, find a new most likely bounding boxes for the targets by + optimized update method using some techniques to speedup calculations specifically for MO TLD. The only limitation + is that all target bounding boxes should have approximately same aspect ratios. Speed boost is around 20% + + @param image The current frame. + + @return True means that all targets were located and false means that tracker couldn't locate one of the targets in + current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed + missing from the frame (say, out of sight) + */ + bool update_opt(InputArray image); +}; + +/*********************************** CSRT ************************************/ +/** @brief the CSRT tracker + +The implementation is based on @cite Lukezic_IJCV2018 Discriminative Correlation Filter with Channel and Spatial Reliability +*/ +class CV_EXPORTS_W TrackerCSRT : public Tracker +{ +public: + struct CV_EXPORTS Params + { + /** + * \brief Constructor + */ + Params(); + + /** + * \brief Read parameters from a file + */ + void read(const FileNode& /*fn*/); + + /** + * \brief Write parameters to a file + */ + void write(cv::FileStorage& fs) const; + + bool use_hog; + bool use_color_names; + bool use_gray; + bool use_rgb; + bool use_channel_weights; + bool use_segmentation; + + std::string window_function; //!< Window function: "hann", "cheb", "kaiser" + float kaiser_alpha; + float cheb_attenuation; + + float template_size; + float gsl_sigma; + float hog_orientations; + float hog_clip; + float padding; + float filter_lr; + float weights_lr; + int num_hog_channels_used; + int admm_iterations; + int histogram_bins; + float histogram_lr; + int background_ratio; + int number_of_scales; + float scale_sigma_factor; + float scale_model_max_area; + float scale_lr; + float scale_step; + + float psr_threshold; //!< we lost the target, if the psr is lower than this. + }; + + /** @brief Constructor + @param parameters CSRT parameters TrackerCSRT::Params + */ + static Ptr create(const TrackerCSRT::Params ¶meters); + + CV_WRAP static Ptr create(); + + CV_WRAP virtual void setInitialMask(InputArray mask) = 0; + + virtual ~TrackerCSRT() CV_OVERRIDE {} +}; + +//! @} +} /* namespace cv */ + +#endif diff --git a/modules/tracking/src/tracker.cpp b/modules/tracking/src/legacy/tracker.legacy.hpp similarity index 100% rename from modules/tracking/src/tracker.cpp rename to modules/tracking/src/legacy/tracker.legacy.hpp diff --git a/modules/tracking/src/trackerCSRT.cpp b/modules/tracking/src/legacy/trackerCSRT.legacy.hpp similarity index 100% rename from modules/tracking/src/trackerCSRT.cpp rename to modules/tracking/src/legacy/trackerCSRT.legacy.hpp diff --git a/modules/tracking/src/trackerKCF.cpp b/modules/tracking/src/legacy/trackerKCF.legacy.hpp similarity index 100% rename from modules/tracking/src/trackerKCF.cpp rename to modules/tracking/src/legacy/trackerKCF.legacy.hpp diff --git a/modules/tracking/src/trackerMIL.cpp b/modules/tracking/src/legacy/trackerMIL.legacy.hpp similarity index 100% rename from modules/tracking/src/trackerMIL.cpp rename to modules/tracking/src/legacy/trackerMIL.legacy.hpp From 80c197590ccfbd1dd9b4f54b250d26e06e840683 Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Tue, 27 Oct 2020 21:18:18 +0000 Subject: [PATCH 20/29] tracking: rework tracking API - simplify Tracker API - fix perf tests (don't measure video decoding) --- modules/tracking/CMakeLists.txt | 23 +- modules/tracking/include/opencv2/tracking.hpp | 271 ++++ .../include/opencv2/tracking/feature.hpp | 13 +- .../opencv2/tracking/kalman_filters.hpp | 19 +- .../opencv2/tracking/onlineBoosting.hpp | 13 +- .../include/opencv2/tracking/onlineMIL.hpp | 9 +- .../include/opencv2/tracking/tldDataset.hpp | 14 +- .../opencv2/tracking/tracking_by_matching.hpp | 11 +- .../opencv2/tracking/tracking_internals.hpp | 799 ++++-------- .../opencv2/tracking/tracking_legacy.hpp | 1118 +---------------- modules/tracking/misc/java/gen_dict.json | 5 + .../java/test/TrackerCreateLegacyTest.java | 23 + .../misc/java/test/TrackerCreateTest.java | 38 + modules/tracking/misc/objc/gen_dict.json | 5 +- .../misc/python/pyopencv_tracking.hpp | 6 + .../misc/python/test/test_tracking_contrib.py | 31 + modules/tracking/perf/perf_Tracker.cpp | 412 ------ modules/tracking/perf/perf_main.cpp | 17 +- modules/tracking/perf/perf_trackers.cpp | 119 ++ modules/tracking/samples/benchmark.cpp | 8 +- modules/tracking/samples/csrt.cpp | 2 +- modules/tracking/samples/goturnTracker.cpp | 11 +- modules/tracking/samples/kcf.cpp | 2 +- .../tracking/samples/multiTracker_dataset.cpp | 4 +- modules/tracking/samples/multitracker.cpp | 6 +- modules/tracking/samples/samples_utility.hpp | 41 +- modules/tracking/samples/tracker.cpp | 8 +- modules/tracking/samples/tracker_dataset.cpp | 8 +- .../tracking/samples/tracking_by_matching.cpp | 5 +- .../tutorial_customizing_cn_tracker.cpp | 2 +- .../tutorial_introduction_to_tracker.cpp | 3 +- .../samples/tutorial_multitracker.cpp | 6 +- modules/tracking/src/PFSolver.hpp | 2 - .../src/augmented_unscented_kalman.cpp | 11 +- modules/tracking/src/feature.cpp | 15 +- modules/tracking/src/featureColorName.cpp | 8 +- modules/tracking/src/gtrTracker.cpp | 144 +-- modules/tracking/src/gtrTracker.hpp | 80 -- modules/tracking/src/gtrUtils.cpp | 1 + modules/tracking/src/gtrUtils.hpp | 1 - modules/tracking/src/kuhn_munkres.cpp | 8 + modules/tracking/src/kuhn_munkres.hpp | 5 + .../tracking/src/legacy/tracker.legacy.hpp | 54 +- .../src/legacy/trackerCSRT.legacy.hpp | 684 +--------- .../tracking/src/legacy/trackerKCF.legacy.hpp | 934 +------------- .../tracking/src/legacy/trackerMIL.legacy.hpp | 264 +--- modules/tracking/src/mosseTracker.cpp | 23 +- modules/tracking/src/multiTracker.cpp | 31 +- modules/tracking/src/multiTracker.hpp | 12 +- modules/tracking/src/multiTracker_alt.cpp | 5 +- modules/tracking/src/onlineBoosting.cpp | 7 +- modules/tracking/src/precomp.hpp | 30 +- modules/tracking/src/tldDataset.cpp | 10 +- modules/tracking/src/tldDetector.cpp | 15 +- modules/tracking/src/tldDetector.hpp | 13 +- .../tracking/src/tldEnsembleClassifier.cpp | 13 +- .../tracking/src/tldEnsembleClassifier.hpp | 14 +- modules/tracking/src/tldModel.cpp | 14 +- modules/tracking/src/tldModel.hpp | 16 +- modules/tracking/src/tldTracker.cpp | 20 +- modules/tracking/src/tldTracker.hpp | 16 +- modules/tracking/src/tldUtils.cpp | 11 +- modules/tracking/src/tldUtils.hpp | 13 +- modules/tracking/src/tracker.cpp | 23 + modules/tracking/src/trackerBoosting.cpp | 10 +- modules/tracking/src/trackerBoostingModel.cpp | 8 +- modules/tracking/src/trackerBoostingModel.hpp | 10 +- modules/tracking/src/trackerCSRT.cpp | 655 ++++++++++ .../src/trackerCSRTScaleEstimation.cpp | 6 +- modules/tracking/src/trackerFeature.cpp | 7 +- modules/tracking/src/trackerFeatureSet.cpp | 8 +- modules/tracking/src/trackerKCF.cpp | 938 ++++++++++++++ modules/tracking/src/trackerMIL.cpp | 265 ++++ modules/tracking/src/trackerMILModel.cpp | 7 +- modules/tracking/src/trackerMILModel.hpp | 9 +- modules/tracking/src/trackerMedianFlow.cpp | 35 +- modules/tracking/src/trackerModel.cpp | 7 +- modules/tracking/src/trackerSampler.cpp | 8 +- .../tracking/src/trackerSamplerAlgorithm.cpp | 17 +- .../tracking/src/trackerStateEstimator.cpp | 7 +- modules/tracking/src/tracking_by_matching.cpp | 10 +- modules/tracking/src/tracking_utils.cpp | 5 +- modules/tracking/src/tracking_utils.hpp | 9 +- modules/tracking/src/unscented_kalman.cpp | 11 +- modules/tracking/test/test_aukf.cpp | 2 +- .../test/test_trackerParametersIO.cpp | 20 +- modules/tracking/test/test_trackers.cpp | 336 +++-- modules/tracking/test/test_ukf.cpp | 2 +- .../tutorials/tutorial_multitracker.markdown | 6 +- 89 files changed, 3621 insertions(+), 4326 deletions(-) create mode 100644 modules/tracking/include/opencv2/tracking.hpp create mode 100644 modules/tracking/misc/java/gen_dict.json create mode 100644 modules/tracking/misc/java/test/TrackerCreateLegacyTest.java create mode 100644 modules/tracking/misc/java/test/TrackerCreateTest.java create mode 100644 modules/tracking/misc/python/pyopencv_tracking.hpp create mode 100644 modules/tracking/misc/python/test/test_tracking_contrib.py delete mode 100644 modules/tracking/perf/perf_Tracker.cpp create mode 100644 modules/tracking/perf/perf_trackers.cpp delete mode 100644 modules/tracking/src/gtrTracker.hpp create mode 100644 modules/tracking/src/tracker.cpp create mode 100644 modules/tracking/src/trackerCSRT.cpp create mode 100644 modules/tracking/src/trackerKCF.cpp create mode 100644 modules/tracking/src/trackerMIL.cpp diff --git a/modules/tracking/CMakeLists.txt b/modules/tracking/CMakeLists.txt index 055a0593ef8..2834b10ca81 100644 --- a/modules/tracking/CMakeLists.txt +++ b/modules/tracking/CMakeLists.txt @@ -1,3 +1,24 @@ set(the_description "Tracking API") -ocv_define_module(tracking opencv_imgproc opencv_core opencv_video opencv_plot OPTIONAL opencv_dnn opencv_datasets WRAP java python objc) + +set(debug_modules "") +if(DEBUG_opencv_tracking) + list(APPEND debug_modules opencv_highgui) +endif() + +ocv_define_module(tracking + opencv_imgproc + opencv_core + opencv_video + opencv_plot # samples only + ${debug_modules} + OPTIONAL + opencv_dnn + opencv_datasets + opencv_highgui + WRAP + java + python + objc +) + ocv_warnings_disable(CMAKE_CXX_FLAGS -Wno-shadow /wd4458) diff --git a/modules/tracking/include/opencv2/tracking.hpp b/modules/tracking/include/opencv2/tracking.hpp new file mode 100644 index 00000000000..f9d1b154f50 --- /dev/null +++ b/modules/tracking/include/opencv2/tracking.hpp @@ -0,0 +1,271 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef OPENCV_CONTRIB_TRACKING_HPP +#define OPENCV_CONTRIB_TRACKING_HPP + +#include "opencv2/core.hpp" + +namespace cv { +#ifndef CV_DOXYGEN +inline namespace tracking { +#endif + +/** @defgroup tracking Tracking API +@{ + @defgroup tracking_detail Tracking API implementation details + @defgroup tracking_legacy Legacy Tracking API +@} +*/ + +/** @addtogroup tracking +@{ +Tracking is an important issue for many computer vision applications in real world scenario. +The development in this area is very fragmented and this API is an interface useful for plug several algorithms and compare them. +*/ + + + +/** @brief Base abstract class for the long-term tracker + */ +class CV_EXPORTS_W Tracker +{ +protected: + Tracker(); +public: + virtual ~Tracker(); + + /** @brief Initialize the tracker with a known bounding box that surrounded the target + @param image The initial frame + @param boundingBox The initial bounding box + */ + CV_WRAP virtual + void init(InputArray image, const Rect& boundingBox) = 0; + + /** @brief Update the tracker, find the new most likely bounding box for the target + @param image The current frame + @param boundingBox The bounding box that represent the new target location, if true was returned, not + modified otherwise + + @return True means that target was located and false means that tracker cannot locate target in + current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed + missing from the frame (say, out of sight) + */ + CV_WRAP virtual + bool update(InputArray image, CV_OUT Rect& boundingBox) = 0; +}; + + +/** @brief the CSRT tracker + +The implementation is based on @cite Lukezic_IJCV2018 Discriminative Correlation Filter with Channel and Spatial Reliability +*/ +class CV_EXPORTS_W TrackerCSRT : public Tracker +{ +protected: + TrackerCSRT(); // use ::create() +public: + virtual ~TrackerCSRT() CV_OVERRIDE; + + struct CV_EXPORTS_W_SIMPLE Params + { + CV_WRAP Params(); + + CV_PROP_RW bool use_hog; + CV_PROP_RW bool use_color_names; + CV_PROP_RW bool use_gray; + CV_PROP_RW bool use_rgb; + CV_PROP_RW bool use_channel_weights; + CV_PROP_RW bool use_segmentation; + + CV_PROP_RW std::string window_function; //!< Window function: "hann", "cheb", "kaiser" + CV_PROP_RW float kaiser_alpha; + CV_PROP_RW float cheb_attenuation; + + CV_PROP_RW float template_size; + CV_PROP_RW float gsl_sigma; + CV_PROP_RW float hog_orientations; + CV_PROP_RW float hog_clip; + CV_PROP_RW float padding; + CV_PROP_RW float filter_lr; + CV_PROP_RW float weights_lr; + CV_PROP_RW int num_hog_channels_used; + CV_PROP_RW int admm_iterations; + CV_PROP_RW int histogram_bins; + CV_PROP_RW float histogram_lr; + CV_PROP_RW int background_ratio; + CV_PROP_RW int number_of_scales; + CV_PROP_RW float scale_sigma_factor; + CV_PROP_RW float scale_model_max_area; + CV_PROP_RW float scale_lr; + CV_PROP_RW float scale_step; + + CV_PROP_RW float psr_threshold; //!< we lost the target, if the psr is lower than this. + }; + + /** @brief Create CSRT tracker instance + @param parameters CSRT parameters TrackerCSRT::Params + */ + static CV_WRAP + Ptr create(const TrackerCSRT::Params ¶meters = TrackerCSRT::Params()); + + //void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + //bool update(InputArray image, CV_OUT Rect& boundingBox) CV_OVERRIDE; + + CV_WRAP virtual void setInitialMask(InputArray mask) = 0; +}; + + + +/** @brief the KCF (Kernelized Correlation Filter) tracker + + * KCF is a novel tracking framework that utilizes properties of circulant matrix to enhance the processing speed. + * This tracking method is an implementation of @cite KCF_ECCV which is extended to KCF with color-names features (@cite KCF_CN). + * The original paper of KCF is available at + * as well as the matlab implementation. For more information about KCF with color-names features, please refer to + * . + */ +class CV_EXPORTS_W TrackerKCF : public Tracker +{ +protected: + TrackerKCF(); // use ::create() +public: + virtual ~TrackerKCF() CV_OVERRIDE; + + /** + * \brief Feature type to be used in the tracking grayscale, colornames, compressed color-names + * The modes available now: + - "GRAY" -- Use grayscale values as the feature + - "CN" -- Color-names feature + */ + enum MODE { + GRAY = (1 << 0), + CN = (1 << 1), + CUSTOM = (1 << 2) + }; + + struct CV_EXPORTS_W_SIMPLE Params + { + CV_WRAP Params(); + + CV_PROP_RW float detect_thresh; //!< detection confidence threshold + CV_PROP_RW float sigma; //!< gaussian kernel bandwidth + CV_PROP_RW float lambda; //!< regularization + CV_PROP_RW float interp_factor; //!< linear interpolation factor for adaptation + CV_PROP_RW float output_sigma_factor; //!< spatial bandwidth (proportional to target) + CV_PROP_RW float pca_learning_rate; //!< compression learning rate + CV_PROP_RW bool resize; //!< activate the resize feature to improve the processing speed + CV_PROP_RW bool split_coeff; //!< split the training coefficients into two matrices + CV_PROP_RW bool wrap_kernel; //!< wrap around the kernel values + CV_PROP_RW bool compress_feature; //!< activate the pca method to compress the features + CV_PROP_RW int max_patch_size; //!< threshold for the ROI size + CV_PROP_RW int compressed_size; //!< feature size after compression + CV_PROP_RW int desc_pca; //!< compressed descriptors of TrackerKCF::MODE + CV_PROP_RW int desc_npca; //!< non-compressed descriptors of TrackerKCF::MODE + }; + + /** @brief Create KCF tracker instance + @param parameters KCF parameters TrackerKCF::Params + */ + static CV_WRAP + Ptr create(const TrackerKCF::Params ¶meters = TrackerKCF::Params()); + + //void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + //bool update(InputArray image, CV_OUT Rect& boundingBox) CV_OVERRIDE; + + // FIXIT use interface + typedef void (*FeatureExtractorCallbackFN)(const Mat, const Rect, Mat&); + virtual void setFeatureExtractor(FeatureExtractorCallbackFN callback, bool pca_func = false) = 0; +}; + + + +/** @brief The MIL algorithm trains a classifier in an online manner to separate the object from the +background. + +Multiple Instance Learning avoids the drift problem for a robust tracking. The implementation is +based on @cite MIL . + +Original code can be found here + */ +class CV_EXPORTS_W TrackerMIL : public Tracker +{ +protected: + TrackerMIL(); // use ::create() +public: + virtual ~TrackerMIL() CV_OVERRIDE; + + struct CV_EXPORTS_W_SIMPLE Params + { + CV_WRAP Params(); + //parameters for sampler + CV_PROP_RW float samplerInitInRadius; //!< radius for gathering positive instances during init + CV_PROP_RW int samplerInitMaxNegNum; //!< # negative samples to use during init + CV_PROP_RW float samplerSearchWinSize; //!< size of search window + CV_PROP_RW float samplerTrackInRadius; //!< radius for gathering positive instances during tracking + CV_PROP_RW int samplerTrackMaxPosNum; //!< # positive samples to use during tracking + CV_PROP_RW int samplerTrackMaxNegNum; //!< # negative samples to use during tracking + CV_PROP_RW int featureSetNumFeatures; //!< # features + }; + + /** @brief Create MIL tracker instance + * @param parameters MIL parameters TrackerMIL::Params + */ + static CV_WRAP + Ptr create(const TrackerMIL::Params ¶meters = TrackerMIL::Params()); + + + //void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + //bool update(InputArray image, CV_OUT Rect& boundingBox) CV_OVERRIDE; +}; + + + +/** @brief the GOTURN (Generic Object Tracking Using Regression Networks) tracker + * + * GOTURN (@cite GOTURN) is kind of trackers based on Convolutional Neural Networks (CNN). While taking all advantages of CNN trackers, + * GOTURN is much faster due to offline training without online fine-tuning nature. + * GOTURN tracker addresses the problem of single target tracking: given a bounding box label of an object in the first frame of the video, + * we track that object through the rest of the video. NOTE: Current method of GOTURN does not handle occlusions; however, it is fairly + * robust to viewpoint changes, lighting changes, and deformations. + * Inputs of GOTURN are two RGB patches representing Target and Search patches resized to 227x227. + * Outputs of GOTURN are predicted bounding box coordinates, relative to Search patch coordinate system, in format X1,Y1,X2,Y2. + * Original paper is here: + * As long as original authors implementation: + * Implementation of training algorithm is placed in separately here due to 3d-party dependencies: + * + * GOTURN architecture goturn.prototxt and trained model goturn.caffemodel are accessible on opencv_extra GitHub repository. + */ +class CV_EXPORTS_W TrackerGOTURN : public Tracker +{ +protected: + TrackerGOTURN(); // use ::create() +public: + virtual ~TrackerGOTURN() CV_OVERRIDE; + + struct CV_EXPORTS_W_SIMPLE Params + { + CV_WRAP Params(); + CV_PROP_RW std::string modelTxt; + CV_PROP_RW std::string modelBin; + }; + + /** @brief Constructor + @param parameters GOTURN parameters TrackerGOTURN::Params + */ + static CV_WRAP + Ptr create(const TrackerGOTURN::Params& parameters = TrackerGOTURN::Params()); + + //void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + //bool update(InputArray image, CV_OUT Rect& boundingBox) CV_OVERRIDE; +}; + +//! @} + +#ifndef CV_DOXYGEN +} +#endif +} // namespace + +#endif // OPENCV_CONTRIB_TRACKING_HPP diff --git a/modules/tracking/include/opencv2/tracking/feature.hpp b/modules/tracking/include/opencv2/tracking/feature.hpp index 3bcfe6e6c7a..ebc28ea944c 100644 --- a/modules/tracking/include/opencv2/tracking/feature.hpp +++ b/modules/tracking/include/opencv2/tracking/feature.hpp @@ -53,12 +53,15 @@ * TODO Changed CvHaarEvaluator based on ADABOOSTING implementation (Grabner et al.) */ -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { -//! @addtogroup tracking +//! @addtogroup tracking_detail //! @{ +inline namespace feature { + #define FEATURES "features" #define CC_FEATURES FEATURES @@ -409,8 +412,10 @@ inline uchar CvLBPEvaluator::Feature::calc( const Mat &_sum, size_t y ) const ( psum[p[4]] - psum[p[5]] - psum[p[8]] + psum[p[9]] >= cval ? 1 : 0 ) ); // 3 } +} // namespace + //! @} -} /* namespace cv */ +}}} // namespace cv #endif diff --git a/modules/tracking/include/opencv2/tracking/kalman_filters.hpp b/modules/tracking/include/opencv2/tracking/kalman_filters.hpp index 7a89c87ddf3..0b2e9da053c 100644 --- a/modules/tracking/include/opencv2/tracking/kalman_filters.hpp +++ b/modules/tracking/include/opencv2/tracking/kalman_filters.hpp @@ -45,10 +45,14 @@ #include "opencv2/core.hpp" #include -namespace cv -{ -namespace tracking -{ +namespace cv { +namespace detail { +inline namespace tracking { + +//! @addtogroup tracking_detail +//! @{ + +inline namespace kalman_filters { /** @brief The interface for Unscented Kalman filter and Augmented Unscented Kalman filter. */ @@ -222,7 +226,10 @@ CV_EXPORTS Ptr createUnscentedKalmanFilter( const Unscent */ CV_EXPORTS Ptr createAugmentedUnscentedKalmanFilter( const AugmentedUnscentedKalmanFilterParams ¶ms ); -} // tracking -} // cv +} // namespace + +//! @} + +}}} // namespace #endif diff --git a/modules/tracking/include/opencv2/tracking/onlineBoosting.hpp b/modules/tracking/include/opencv2/tracking/onlineBoosting.hpp index 982bc205b17..a8638c542df 100644 --- a/modules/tracking/include/opencv2/tracking/onlineBoosting.hpp +++ b/modules/tracking/include/opencv2/tracking/onlineBoosting.hpp @@ -44,12 +44,15 @@ #include "opencv2/core.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { -//! @addtogroup tracking +//! @addtogroup tracking_detail //! @{ +inline namespace online_boosting { + //TODO based on the original implementation //http://vision.ucsd.edu/~bbabenko/project_miltrack.shtml @@ -281,8 +284,10 @@ class ClassifierThreshold int m_parity; }; +} // namespace + //! @} -} /* namespace cv */ +}}} // namespace #endif diff --git a/modules/tracking/include/opencv2/tracking/onlineMIL.hpp b/modules/tracking/include/opencv2/tracking/onlineMIL.hpp index 78e1372f8a3..9fb341eeba8 100644 --- a/modules/tracking/include/opencv2/tracking/onlineMIL.hpp +++ b/modules/tracking/include/opencv2/tracking/onlineMIL.hpp @@ -45,10 +45,11 @@ #include "opencv2/core.hpp" #include -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { -//! @addtogroup tracking +//! @addtogroup tracking_detail //! @{ //TODO based on the original implementation @@ -113,6 +114,6 @@ class ClfOnlineStump //! @} -} /* namespace cv */ +}}} // namespace #endif diff --git a/modules/tracking/include/opencv2/tracking/tldDataset.hpp b/modules/tracking/include/opencv2/tracking/tldDataset.hpp index a874255669a..1bdd3fb4c48 100644 --- a/modules/tracking/include/opencv2/tracking/tldDataset.hpp +++ b/modules/tracking/include/opencv2/tracking/tldDataset.hpp @@ -44,13 +44,21 @@ #include "opencv2/core.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { + +//! @addtogroup tracking_detail +//! @{ + namespace tld { CV_EXPORTS cv::Rect2d tld_InitDataset(int videoInd, const char* rootPath = "TLD_dataset", int datasetInd = 0); CV_EXPORTS cv::String tld_getNextDatasetFrame(); } -} + +//! @} + +}}} #endif diff --git a/modules/tracking/include/opencv2/tracking/tracking_by_matching.hpp b/modules/tracking/include/opencv2/tracking/tracking_by_matching.hpp index b6962e2a9b1..6590c2a1eef 100644 --- a/modules/tracking/include/opencv2/tracking/tracking_by_matching.hpp +++ b/modules/tracking/include/opencv2/tracking/tracking_by_matching.hpp @@ -20,6 +20,12 @@ namespace cv { +namespace detail { +inline namespace tracking { + +//! @addtogroup tracking_detail +//! @{ + namespace tbm { //Tracking-by-Matching /// /// \brief The TrackedObject struct defines properties of detected object. @@ -553,5 +559,8 @@ class CV_EXPORTS ITrackerByMatching { CV_EXPORTS cv::Ptr createTrackerByMatching(const TrackerParams ¶ms = TrackerParams()); } // namespace tbm -} // namespace cv + +//! @} + +}}} // namespace #endif // #ifndef __OPENCV_TRACKING_TRACKING_BY_MATCHING_HPP__ diff --git a/modules/tracking/include/opencv2/tracking/tracking_internals.hpp b/modules/tracking/include/opencv2/tracking/tracking_internals.hpp index 3c09c2b771b..fee43e3e086 100644 --- a/modules/tracking/include/opencv2/tracking/tracking_internals.hpp +++ b/modules/tracking/include/opencv2/tracking/tracking_internals.hpp @@ -39,30 +39,264 @@ // //M*/ -#ifndef __OPENCV_TRACKER_HPP__ -#define __OPENCV_TRACKER_HPP__ - -#include "opencv2/core.hpp" -#include "opencv2/imgproc/types_c.h" -#include "feature.hpp" -#include "onlineMIL.hpp" -#include "onlineBoosting.hpp" +#ifndef OPENCV_TRACKING_DETAIL_HPP +#define OPENCV_TRACKING_DETAIL_HPP /* * Partially based on: * ==================================================================================================================== - * - [AAM] S. Salti, A. Cavallaro, L. Di Stefano, Adaptive Appearance Modeling for Video Tracking: Survey and Evaluation + * - [AAM] S. Salti, A. Cavallaro, L. Di Stefano, Adaptive Appearance Modeling for Video Tracking: Survey and Evaluation * - [AMVOT] X. Li, W. Hu, C. Shen, Z. Zhang, A. Dick, A. van den Hengel, A Survey of Appearance Models in Visual Object Tracking * * This Tracking API has been designed with PlantUML. If you modify this API please change UML files under modules/tracking/doc/uml * */ -namespace cv -{ +#include "opencv2/core.hpp" + +#include "feature.hpp" // CvHaarEvaluator +#include "onlineBoosting.hpp" // StrongClassifierDirectSelection +#include "onlineMIL.hpp" // ClfMilBoost + +namespace cv { +namespace detail { +inline namespace tracking { + +/** @addtogroup tracking_detail +@{ + +Long-term optical tracking API +------------------------------ + +Long-term optical tracking is an important issue for many computer vision applications in +real world scenario. The development in this area is very fragmented and this API is an unique +interface useful for plug several algorithms and compare them. This work is partially based on +@cite AAM and @cite AMVOT . + +These algorithms start from a bounding box of the target and with their internal representation they +avoid the drift during the tracking. These long-term trackers are able to evaluate online the +quality of the location of the target in the new frame, without ground truth. + +There are three main components: the TrackerSampler, the TrackerFeatureSet and the TrackerModel. The +first component is the object that computes the patches over the frame based on the last target +location. The TrackerFeatureSet is the class that manages the Features, is possible plug many kind +of these (HAAR, HOG, LBP, Feature2D, etc). The last component is the internal representation of the +target, it is the appearance model. It stores all state candidates and compute the trajectory (the +most likely target states). The class TrackerTargetState represents a possible state of the target. +The TrackerSampler and the TrackerFeatureSet are the visual representation of the target, instead +the TrackerModel is the statistical model. + +A recent benchmark between these algorithms can be found in @cite OOT + +Creating Your Own %Tracker +-------------------- + +If you want to create a new tracker, here's what you have to do. First, decide on the name of the class +for the tracker (to meet the existing style, we suggest something with prefix "tracker", e.g. +trackerMIL, trackerBoosting) -- we shall refer to this choice as to "classname" in subsequent. + +- Declare your tracker in modules/tracking/include/opencv2/tracking/tracker.hpp. Your tracker should inherit from + Tracker (please, see the example below). You should declare the specialized Param structure, + where you probably will want to put the data, needed to initialize your tracker. You should + get something similar to : +@code + class CV_EXPORTS_W TrackerMIL : public Tracker + { + public: + struct CV_EXPORTS Params + { + Params(); + //parameters for sampler + float samplerInitInRadius; // radius for gathering positive instances during init + int samplerInitMaxNegNum; // # negative samples to use during init + float samplerSearchWinSize; // size of search window + float samplerTrackInRadius; // radius for gathering positive instances during tracking + int samplerTrackMaxPosNum; // # positive samples to use during tracking + int samplerTrackMaxNegNum; // # negative samples to use during tracking + int featureSetNumFeatures; // #features + + void read( const FileNode& fn ); + void write( FileStorage& fs ) const; + }; +@endcode + of course, you can also add any additional methods of your choice. It should be pointed out, + however, that it is not expected to have a constructor declared, as creation should be done via + the corresponding create() method. +- Finally, you should implement the function with signature : +@code + Ptr classname::create(const classname::Params ¶meters){ + ... + } +@endcode + That function can (and probably will) return a pointer to some derived class of "classname", + which will probably have a real constructor. + +Every tracker has three component TrackerSampler, TrackerFeatureSet and TrackerModel. The first two +are instantiated from Tracker base class, instead the last component is abstract, so you must +implement your TrackerModel. + +### TrackerSampler + +TrackerSampler is already instantiated, but you should define the sampling algorithm and add the +classes (or single class) to TrackerSampler. You can choose one of the ready implementation as +TrackerSamplerCSC or you can implement your sampling method, in this case the class must inherit +TrackerSamplerAlgorithm. Fill the samplingImpl method that writes the result in "sample" output +argument. + +Example of creating specialized TrackerSamplerAlgorithm TrackerSamplerCSC : : +@code + class CV_EXPORTS_W TrackerSamplerCSC : public TrackerSamplerAlgorithm + { + public: + TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meters = TrackerSamplerCSC::Params() ); + ~TrackerSamplerCSC(); + ... + + protected: + bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ); + ... + + }; +@endcode + +Example of adding TrackerSamplerAlgorithm to TrackerSampler : : +@code + //sampler is the TrackerSampler + Ptr CSCSampler = new TrackerSamplerCSC( CSCparameters ); + if( !sampler->addTrackerSamplerAlgorithm( CSCSampler ) ) + return false; + + //or add CSC sampler with default parameters + //sampler->addTrackerSamplerAlgorithm( "CSC" ); +@endcode +@sa + TrackerSamplerCSC, TrackerSamplerAlgorithm + +### TrackerFeatureSet + +TrackerFeatureSet is already instantiated (as first) , but you should define what kinds of features +you'll use in your tracker. You can use multiple feature types, so you can add a ready +implementation as TrackerFeatureHAAR in your TrackerFeatureSet or develop your own implementation. +In this case, in the computeImpl method put the code that extract the features and in the selection +method optionally put the code for the refinement and selection of the features. + +Example of creating specialized TrackerFeature TrackerFeatureHAAR : : +@code + class CV_EXPORTS_W TrackerFeatureHAAR : public TrackerFeature + { + public: + TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶meters = TrackerFeatureHAAR::Params() ); + ~TrackerFeatureHAAR(); + void selection( Mat& response, int npoints ); + ... + + protected: + bool computeImpl( const std::vector& images, Mat& response ); + ... + + }; +@endcode +Example of adding TrackerFeature to TrackerFeatureSet : : +@code + //featureSet is the TrackerFeatureSet + Ptr trackerFeature = new TrackerFeatureHAAR( HAARparameters ); + featureSet->addTrackerFeature( trackerFeature ); +@endcode +@sa + TrackerFeatureHAAR, TrackerFeatureSet + +### TrackerModel + +TrackerModel is abstract, so in your implementation you must develop your TrackerModel that inherit +from TrackerModel. Fill the method for the estimation of the state "modelEstimationImpl", that +estimates the most likely target location, see @cite AAM table I (ME) for further information. Fill +"modelUpdateImpl" in order to update the model, see @cite AAM table I (MU). In this class you can use +the :cConfidenceMap and :cTrajectory to storing the model. The first represents the model on the all +possible candidate states and the second represents the list of all estimated states. + +Example of creating specialized TrackerModel TrackerMILModel : : +@code + class TrackerMILModel : public TrackerModel + { + public: + TrackerMILModel( const Rect& boundingBox ); + ~TrackerMILModel(); + ... + + protected: + void modelEstimationImpl( const std::vector& responses ); + void modelUpdateImpl(); + ... + + }; +@endcode +And add it in your Tracker : : +@code + bool TrackerMIL::initImpl( const Mat& image, const Rect2d& boundingBox ) + { + ... + //model is the general TrackerModel field of the general Tracker + model = new TrackerMILModel( boundingBox ); + ... + } +@endcode +In the last step you should define the TrackerStateEstimator based on your implementation or you can +use one of ready class as TrackerStateEstimatorMILBoosting. It represent the statistical part of the +model that estimates the most likely target state. + +Example of creating specialized TrackerStateEstimator TrackerStateEstimatorMILBoosting : : +@code + class CV_EXPORTS_W TrackerStateEstimatorMILBoosting : public TrackerStateEstimator + { + class TrackerMILTargetState : public TrackerTargetState + { + ... + }; + + public: + TrackerStateEstimatorMILBoosting( int nFeatures = 250 ); + ~TrackerStateEstimatorMILBoosting(); + ... + + protected: + Ptr estimateImpl( const std::vector& confidenceMaps ); + void updateImpl( std::vector& confidenceMaps ); + ... + + }; +@endcode +And add it in your TrackerModel : : +@code + //model is the TrackerModel of your Tracker + Ptr stateEstimator = new TrackerStateEstimatorMILBoosting( params.featureSetNumFeatures ); + model->setTrackerStateEstimator( stateEstimator ); +@endcode +@sa + TrackerModel, TrackerStateEstimatorMILBoosting, TrackerTargetState + +During this step, you should define your TrackerTargetState based on your implementation. +TrackerTargetState base class has only the bounding box (upper-left position, width and height), you +can enrich it adding scale factor, target rotation, etc. + +Example of creating specialized TrackerTargetState TrackerMILTargetState : : +@code + class TrackerMILTargetState : public TrackerTargetState + { + public: + TrackerMILTargetState( const Point2f& position, int targetWidth, int targetHeight, bool foreground, const Mat& features ); + ~TrackerMILTargetState(); + ... + + private: + bool isTarget; + Mat targetFeatures; + ... + + }; +@endcode + +*/ -//! @addtogroup tracking -//! @{ /************************************ TrackerFeature Base Classes ************************************/ @@ -517,50 +751,6 @@ class CV_EXPORTS TrackerModel }; -/************************************ Tracker Base Class ************************************/ - -/** @brief Base abstract class for the long-term tracker: - */ -class CV_EXPORTS_W Tracker : public virtual Algorithm -{ - public: - - virtual ~Tracker() CV_OVERRIDE; - - /** @brief Initialize the tracker with a known bounding box that surrounded the target - @param image The initial frame - @param boundingBox The initial bounding box - - @return True if initialization went succesfully, false otherwise - */ - CV_WRAP bool init( InputArray image, const Rect2d& boundingBox ); - - /** @brief Update the tracker, find the new most likely bounding box for the target - @param image The current frame - @param boundingBox The bounding box that represent the new target location, if true was returned, not - modified otherwise - - @return True means that target was located and false means that tracker cannot locate target in - current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed - missing from the frame (say, out of sight) - */ - CV_WRAP bool update( InputArray image, CV_OUT Rect2d& boundingBox ); - - virtual void read( const FileNode& fn ) CV_OVERRIDE = 0; - virtual void write( FileStorage& fs ) const CV_OVERRIDE = 0; - - protected: - - virtual bool initImpl( const Mat& image, const Rect2d& boundingBox ) = 0; - virtual bool updateImpl( const Mat& image, Rect2d& boundingBox ) = 0; - - bool isInit; - - Ptr featureSet; - Ptr sampler; - Ptr model; -}; - /************************************ Specific TrackerStateEstimator Classes ************************************/ @@ -1052,497 +1242,8 @@ class CV_EXPORTS TrackerFeatureLBP : public TrackerFeature }; -/************************************ Specific Tracker Classes ************************************/ - -/** @brief The MIL algorithm trains a classifier in an online manner to separate the object from the -background. - -Multiple Instance Learning avoids the drift problem for a robust tracking. The implementation is -based on @cite MIL . - -Original code can be found here - */ -class CV_EXPORTS_W TrackerMIL : public Tracker -{ - public: - struct CV_EXPORTS Params - { - Params(); - //parameters for sampler - float samplerInitInRadius; //!< radius for gathering positive instances during init - int samplerInitMaxNegNum; //!< # negative samples to use during init - float samplerSearchWinSize; //!< size of search window - float samplerTrackInRadius; //!< radius for gathering positive instances during tracking - int samplerTrackMaxPosNum; //!< # positive samples to use during tracking - int samplerTrackMaxNegNum; //!< # negative samples to use during tracking - int featureSetNumFeatures; //!< # features - - void read( const FileNode& fn ); - void write( FileStorage& fs ) const; - }; - - /** @brief Constructor - @param parameters MIL parameters TrackerMIL::Params - */ - static Ptr create(const TrackerMIL::Params ¶meters); - - CV_WRAP static Ptr create(); - - virtual ~TrackerMIL() CV_OVERRIDE {} -}; - -/** @brief the Boosting tracker - -This is a real-time object tracking based on a novel on-line version of the AdaBoost algorithm. -The classifier uses the surrounding background as negative examples in update step to avoid the -drifting problem. The implementation is based on @cite OLB . - */ -class CV_EXPORTS_W TrackerBoosting : public Tracker -{ - public: - struct CV_EXPORTS Params - { - Params(); - int numClassifiers; //! create(const TrackerBoosting::Params ¶meters); - - CV_WRAP static Ptr create(); - - virtual ~TrackerBoosting() CV_OVERRIDE {} -}; - -/** @brief the Median Flow tracker - -Implementation of a paper @cite MedianFlow . - -The tracker is suitable for very smooth and predictable movements when object is visible throughout -the whole sequence. It's quite and accurate for this type of problems (in particular, it was shown -by authors to outperform MIL). During the implementation period the code at -, the courtesy of the author Arthur Amarra, was used for the -reference purpose. - */ -class CV_EXPORTS_W TrackerMedianFlow : public Tracker -{ - public: - struct CV_EXPORTS Params - { - Params(); //! create(const TrackerMedianFlow::Params ¶meters); - - CV_WRAP static Ptr create(); - - virtual ~TrackerMedianFlow() CV_OVERRIDE {} -}; - -/** @brief the TLD (Tracking, learning and detection) tracker - -TLD is a novel tracking framework that explicitly decomposes the long-term tracking task into -tracking, learning and detection. - -The tracker follows the object from frame to frame. The detector localizes all appearances that -have been observed so far and corrects the tracker if necessary. The learning estimates detector's -errors and updates it to avoid these errors in the future. The implementation is based on @cite TLD . - -The Median Flow algorithm (see cv::TrackerMedianFlow) was chosen as a tracking component in this -implementation, following authors. The tracker is supposed to be able to handle rapid motions, partial -occlusions, object absence etc. - */ -class CV_EXPORTS_W TrackerTLD : public Tracker -{ - public: - struct CV_EXPORTS Params - { - Params(); - void read( const FileNode& /*fn*/ ); - void write( FileStorage& /*fs*/ ) const; - }; - - /** @brief Constructor - @param parameters TLD parameters TrackerTLD::Params - */ - static Ptr create(const TrackerTLD::Params ¶meters); - - CV_WRAP static Ptr create(); - - virtual ~TrackerTLD() CV_OVERRIDE {} -}; - -/** @brief the KCF (Kernelized Correlation Filter) tracker - - * KCF is a novel tracking framework that utilizes properties of circulant matrix to enhance the processing speed. - * This tracking method is an implementation of @cite KCF_ECCV which is extended to KCF with color-names features (@cite KCF_CN). - * The original paper of KCF is available at - * as well as the matlab implementation. For more information about KCF with color-names features, please refer to - * . - */ -class CV_EXPORTS_W TrackerKCF : public Tracker -{ -public: - /** - * \brief Feature type to be used in the tracking grayscale, colornames, compressed color-names - * The modes available now: - - "GRAY" -- Use grayscale values as the feature - - "CN" -- Color-names feature - */ - enum MODE { - GRAY = (1 << 0), - CN = (1 << 1), - CUSTOM = (1 << 2) - }; - - struct CV_EXPORTS Params - { - /** - * \brief Constructor - */ - Params(); - - /** - * \brief Read parameters from a file - */ - void read(const FileNode& /*fn*/); - - /** - * \brief Write parameters to a file - */ - void write(FileStorage& /*fs*/) const; - - float detect_thresh; //!< detection confidence threshold - float sigma; //!< gaussian kernel bandwidth - float lambda; //!< regularization - float interp_factor; //!< linear interpolation factor for adaptation - float output_sigma_factor; //!< spatial bandwidth (proportional to target) - float pca_learning_rate; //!< compression learning rate - bool resize; //!< activate the resize feature to improve the processing speed - bool split_coeff; //!< split the training coefficients into two matrices - bool wrap_kernel; //!< wrap around the kernel values - bool compress_feature; //!< activate the pca method to compress the features - int max_patch_size; //!< threshold for the ROI size - int compressed_size; //!< feature size after compression - int desc_pca; //!< compressed descriptors of TrackerKCF::MODE - int desc_npca; //!< non-compressed descriptors of TrackerKCF::MODE - }; - - virtual void setFeatureExtractor(void(*)(const Mat, const Rect, Mat&), bool pca_func = false) = 0; - - /** @brief Constructor - @param parameters KCF parameters TrackerKCF::Params - */ - static Ptr create(const TrackerKCF::Params ¶meters); - - CV_WRAP static Ptr create(); - - virtual ~TrackerKCF() CV_OVERRIDE {} -}; - -/** @brief the GOTURN (Generic Object Tracking Using Regression Networks) tracker - - * GOTURN (@cite GOTURN) is kind of trackers based on Convolutional Neural Networks (CNN). While taking all advantages of CNN trackers, - * GOTURN is much faster due to offline training without online fine-tuning nature. - * GOTURN tracker addresses the problem of single target tracking: given a bounding box label of an object in the first frame of the video, - * we track that object through the rest of the video. NOTE: Current method of GOTURN does not handle occlusions; however, it is fairly - * robust to viewpoint changes, lighting changes, and deformations. - * Inputs of GOTURN are two RGB patches representing Target and Search patches resized to 227x227. - * Outputs of GOTURN are predicted bounding box coordinates, relative to Search patch coordinate system, in format X1,Y1,X2,Y2. - * Original paper is here: - * As long as original authors implementation: - * Implementation of training algorithm is placed in separately here due to 3d-party dependencies: - * - * GOTURN architecture goturn.prototxt and trained model goturn.caffemodel are accessible on opencv_extra GitHub repository. -*/ -class CV_EXPORTS_W TrackerGOTURN : public Tracker -{ -public: - struct CV_EXPORTS Params - { - Params(); - void read(const FileNode& /*fn*/); - void write(FileStorage& /*fs*/) const; - String modelTxt; - String modelBin; - }; - - /** @brief Constructor - @param parameters GOTURN parameters TrackerGOTURN::Params - */ - static Ptr create(const TrackerGOTURN::Params ¶meters); - - CV_WRAP static Ptr create(); - - virtual ~TrackerGOTURN() CV_OVERRIDE {} -}; - -/** @brief the MOSSE (Minimum Output Sum of Squared %Error) tracker - -The implementation is based on @cite MOSSE Visual Object Tracking using Adaptive Correlation Filters -@note this tracker works with grayscale images, if passed bgr ones, they will get converted internally. -*/ - -class CV_EXPORTS_W TrackerMOSSE : public Tracker -{ - public: - /** @brief Constructor - */ - CV_WRAP static Ptr create(); - - virtual ~TrackerMOSSE() CV_OVERRIDE {} -}; - - -/************************************ MultiTracker Class ---By Laksono Kurnianggoro---) ************************************/ -/** @brief This class is used to track multiple objects using the specified tracker algorithm. - -* The %MultiTracker is naive implementation of multiple object tracking. -* It process the tracked objects independently without any optimization accross the tracked objects. -*/ -class CV_EXPORTS_W MultiTracker : public Algorithm -{ -public: - - /** - * \brief Constructor. - */ - CV_WRAP MultiTracker(); - - /** - * \brief Destructor - */ - ~MultiTracker() CV_OVERRIDE; - - /** - * \brief Add a new object to be tracked. - * - * @param newTracker tracking algorithm to be used - * @param image input image - * @param boundingBox a rectangle represents ROI of the tracked object - */ - CV_WRAP bool add(Ptr newTracker, InputArray image, const Rect2d& boundingBox); - - /** - * \brief Add a set of objects to be tracked. - * @param newTrackers list of tracking algorithms to be used - * @param image input image - * @param boundingBox list of the tracked objects - */ - bool add(std::vector > newTrackers, InputArray image, std::vector boundingBox); - - /** - * \brief Update the current tracking status. - * The result will be saved in the internal storage. - * @param image input image - */ - bool update(InputArray image); - - /** - * \brief Update the current tracking status. - * @param image input image - * @param boundingBox the tracking result, represent a list of ROIs of the tracked objects. - */ - CV_WRAP bool update(InputArray image, CV_OUT std::vector & boundingBox); - - /** - * \brief Returns a reference to a storage for the tracked objects, each object corresponds to one tracker algorithm - */ - CV_WRAP const std::vector& getObjects() const; - - /** - * \brief Returns a pointer to a new instance of MultiTracker - */ - CV_WRAP static Ptr create(); - -protected: - //!< storage for the tracker algorithms. - std::vector< Ptr > trackerList; - - //!< storage for the tracked objects, each object corresponds to one tracker algorithm. - std::vector objects; -}; - -/************************************ Multi-Tracker Classes ---By Tyan Vladimir---************************************/ - -/** @brief Base abstract class for the long-term Multi Object Trackers: - -@sa Tracker, MultiTrackerTLD -*/ -class CV_EXPORTS MultiTracker_Alt -{ -public: - /** @brief Constructor for Multitracker - */ - MultiTracker_Alt() - { - targetNum = 0; - } - - /** @brief Add a new target to a tracking-list and initialize the tracker with a known bounding box that surrounded the target - @param image The initial frame - @param boundingBox The initial bounding box of target - @param tracker_algorithm Multi-tracker algorithm - - @return True if new target initialization went succesfully, false otherwise - */ - bool addTarget(InputArray image, const Rect2d& boundingBox, Ptr tracker_algorithm); - - /** @brief Update all trackers from the tracking-list, find a new most likely bounding boxes for the targets - @param image The current frame - - @return True means that all targets were located and false means that tracker couldn't locate one of the targets in - current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed - missing from the frame (say, out of sight) - */ - bool update(InputArray image); - - /** @brief Current number of targets in tracking-list - */ - int targetNum; - - /** @brief Trackers list for Multi-Object-Tracker - */ - std::vector > trackers; - - /** @brief Bounding Boxes list for Multi-Object-Tracker - */ - std::vector boundingBoxes; - /** @brief List of randomly generated colors for bounding boxes display - */ - std::vector colors; -}; - -/** @brief Multi Object %Tracker for TLD. - -TLD is a novel tracking framework that explicitly decomposes -the long-term tracking task into tracking, learning and detection. - -The tracker follows the object from frame to frame. The detector localizes all appearances that -have been observed so far and corrects the tracker if necessary. The learning estimates detector's -errors and updates it to avoid these errors in the future. The implementation is based on @cite TLD . - -The Median Flow algorithm (see cv::TrackerMedianFlow) was chosen as a tracking component in this -implementation, following authors. The tracker is supposed to be able to handle rapid motions, partial -occlusions, object absence etc. - -@sa Tracker, MultiTracker, TrackerTLD -*/ -class CV_EXPORTS MultiTrackerTLD : public MultiTracker_Alt -{ -public: - /** @brief Update all trackers from the tracking-list, find a new most likely bounding boxes for the targets by - optimized update method using some techniques to speedup calculations specifically for MO TLD. The only limitation - is that all target bounding boxes should have approximately same aspect ratios. Speed boost is around 20% - - @param image The current frame. - - @return True means that all targets were located and false means that tracker couldn't locate one of the targets in - current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed - missing from the frame (say, out of sight) - */ - bool update_opt(InputArray image); -}; - -/*********************************** CSRT ************************************/ -/** @brief the CSRT tracker - -The implementation is based on @cite Lukezic_IJCV2018 Discriminative Correlation Filter with Channel and Spatial Reliability -*/ -class CV_EXPORTS_W TrackerCSRT : public Tracker -{ -public: - struct CV_EXPORTS Params - { - /** - * \brief Constructor - */ - Params(); - - /** - * \brief Read parameters from a file - */ - void read(const FileNode& /*fn*/); - - /** - * \brief Write parameters to a file - */ - void write(cv::FileStorage& fs) const; - - bool use_hog; - bool use_color_names; - bool use_gray; - bool use_rgb; - bool use_channel_weights; - bool use_segmentation; - - std::string window_function; //!< Window function: "hann", "cheb", "kaiser" - float kaiser_alpha; - float cheb_attenuation; - - float template_size; - float gsl_sigma; - float hog_orientations; - float hog_clip; - float padding; - float filter_lr; - float weights_lr; - int num_hog_channels_used; - int admm_iterations; - int histogram_bins; - float histogram_lr; - int background_ratio; - int number_of_scales; - float scale_sigma_factor; - float scale_model_max_area; - float scale_lr; - float scale_step; - - float psr_threshold; //!< we lost the target, if the psr is lower than this. - }; - - /** @brief Constructor - @param parameters CSRT parameters TrackerCSRT::Params - */ - static Ptr create(const TrackerCSRT::Params ¶meters); - - CV_WRAP static Ptr create(); - - CV_WRAP virtual void setInitialMask(InputArray mask) = 0; - - virtual ~TrackerCSRT() CV_OVERRIDE {} -}; - //! @} -} /* namespace cv */ -#endif +}}} // namespace + +#endif // OPENCV_TRACKING_DETAIL_HPP diff --git a/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp b/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp index 3c09c2b771b..bf0bef33c23 100644 --- a/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp +++ b/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp @@ -39,483 +39,31 @@ // //M*/ -#ifndef __OPENCV_TRACKER_HPP__ -#define __OPENCV_TRACKER_HPP__ - -#include "opencv2/core.hpp" -#include "opencv2/imgproc/types_c.h" -#include "feature.hpp" -#include "onlineMIL.hpp" -#include "onlineBoosting.hpp" +#ifndef OPENCV_TRACKING_LEGACY_HPP +#define OPENCV_TRACKING_LEGACY_HPP /* * Partially based on: * ==================================================================================================================== - * - [AAM] S. Salti, A. Cavallaro, L. Di Stefano, Adaptive Appearance Modeling for Video Tracking: Survey and Evaluation + * - [AAM] S. Salti, A. Cavallaro, L. Di Stefano, Adaptive Appearance Modeling for Video Tracking: Survey and Evaluation * - [AMVOT] X. Li, W. Hu, C. Shen, Z. Zhang, A. Dick, A. van den Hengel, A Survey of Appearance Models in Visual Object Tracking * * This Tracking API has been designed with PlantUML. If you modify this API please change UML files under modules/tracking/doc/uml * */ -namespace cv -{ - -//! @addtogroup tracking -//! @{ - -/************************************ TrackerFeature Base Classes ************************************/ - -/** @brief Abstract base class for TrackerFeature that represents the feature. - */ -class CV_EXPORTS TrackerFeature -{ - public: - virtual ~TrackerFeature(); - - /** @brief Compute the features in the images collection - @param images The images - @param response The output response - */ - void compute( const std::vector& images, Mat& response ); - - /** @brief Create TrackerFeature by tracker feature type - @param trackerFeatureType The TrackerFeature name - - The modes available now: - - - "HAAR" -- Haar Feature-based - - The modes that will be available soon: - - - "HOG" -- Histogram of Oriented Gradients features - - "LBP" -- Local Binary Pattern features - - "FEATURE2D" -- All types of Feature2D - */ - static Ptr create( const String& trackerFeatureType ); - - /** @brief Identify most effective features - @param response Collection of response for the specific TrackerFeature - @param npoints Max number of features - - @note This method modifies the response parameter - */ - virtual void selection( Mat& response, int npoints ) = 0; - - /** @brief Get the name of the specific TrackerFeature - */ - String getClassName() const; - - protected: - - virtual bool computeImpl( const std::vector& images, Mat& response ) = 0; - - String className; -}; - -/** @brief Class that manages the extraction and selection of features - -@cite AAM Feature Extraction and Feature Set Refinement (Feature Processing and Feature Selection). -See table I and section III C @cite AMVOT Appearance modelling -\> Visual representation (Table II, -section 3.1 - 3.2) - -TrackerFeatureSet is an aggregation of TrackerFeature - -@sa - TrackerFeature - - */ -class CV_EXPORTS TrackerFeatureSet -{ - public: - - TrackerFeatureSet(); - - ~TrackerFeatureSet(); - - /** @brief Extract features from the images collection - @param images The input images - */ - void extraction( const std::vector& images ); - - /** @brief Identify most effective features for all feature types (optional) - */ - void selection(); - - /** @brief Remove outliers for all feature types (optional) - */ - void removeOutliers(); - - /** @brief Add TrackerFeature in the collection. Return true if TrackerFeature is added, false otherwise - @param trackerFeatureType The TrackerFeature name - - The modes available now: - - - "HAAR" -- Haar Feature-based - - The modes that will be available soon: - - - "HOG" -- Histogram of Oriented Gradients features - - "LBP" -- Local Binary Pattern features - - "FEATURE2D" -- All types of Feature2D - - Example TrackerFeatureSet::addTrackerFeature : : - @code - //sample usage: - - Ptr trackerFeature = new TrackerFeatureHAAR( HAARparameters ); - featureSet->addTrackerFeature( trackerFeature ); - - //or add CSC sampler with default parameters - //featureSet->addTrackerFeature( "HAAR" ); - @endcode - @note If you use the second method, you must initialize the TrackerFeature - */ - bool addTrackerFeature( String trackerFeatureType ); - - /** @overload - @param feature The TrackerFeature class - */ - bool addTrackerFeature( Ptr& feature ); - - /** @brief Get the TrackerFeature collection (TrackerFeature name, TrackerFeature pointer) - */ - const std::vector > >& getTrackerFeature() const; - - /** @brief Get the responses - - @note Be sure to call extraction before getResponses Example TrackerFeatureSet::getResponses : : - */ - const std::vector& getResponses() const; - - private: - - void clearResponses(); - bool blockAddTrackerFeature; - - std::vector > > features; //list of features - std::vector responses; //list of response after compute - -}; - -/************************************ TrackerSampler Base Classes ************************************/ - -/** @brief Abstract base class for TrackerSamplerAlgorithm that represents the algorithm for the specific -sampler. - */ -class CV_EXPORTS TrackerSamplerAlgorithm -{ - public: - /** - * \brief Destructor - */ - virtual ~TrackerSamplerAlgorithm(); - - /** @brief Create TrackerSamplerAlgorithm by tracker sampler type. - @param trackerSamplerType The trackerSamplerType name - - The modes available now: - - - "CSC" -- Current State Center - - "CS" -- Current State - */ - static Ptr create( const String& trackerSamplerType ); - - /** @brief Computes the regions starting from a position in an image. - - Return true if samples are computed, false otherwise - - @param image The current frame - @param boundingBox The bounding box from which regions can be calculated - - @param sample The computed samples @cite AAM Fig. 1 variable Sk - */ - bool sampling( const Mat& image, Rect boundingBox, std::vector& sample ); - - /** @brief Get the name of the specific TrackerSamplerAlgorithm - */ - String getClassName() const; - - protected: - String className; - - virtual bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) = 0; -}; - -/** - * \brief Class that manages the sampler in order to select regions for the update the model of the tracker - * [AAM] Sampling e Labeling. See table I and section III B - */ - -/** @brief Class that manages the sampler in order to select regions for the update the model of the tracker - -@cite AAM Sampling e Labeling. See table I and section III B - -TrackerSampler is an aggregation of TrackerSamplerAlgorithm -@sa - TrackerSamplerAlgorithm - */ -class CV_EXPORTS TrackerSampler -{ - public: - - /** - * \brief Constructor - */ - TrackerSampler(); - - /** - * \brief Destructor - */ - ~TrackerSampler(); - - /** @brief Computes the regions starting from a position in an image - @param image The current frame - @param boundingBox The bounding box from which regions can be calculated - */ - void sampling( const Mat& image, Rect boundingBox ); - - /** @brief Return the collection of the TrackerSamplerAlgorithm - */ - const std::vector > >& getSamplers() const; - - /** @brief Return the samples from all TrackerSamplerAlgorithm, @cite AAM Fig. 1 variable Sk - */ - const std::vector& getSamples() const; - - /** @brief Add TrackerSamplerAlgorithm in the collection. Return true if sampler is added, false otherwise - @param trackerSamplerAlgorithmType The TrackerSamplerAlgorithm name - - The modes available now: - - "CSC" -- Current State Center - - "CS" -- Current State - - "PF" -- Particle Filtering - - Example TrackerSamplerAlgorithm::addTrackerSamplerAlgorithm : : - @code - TrackerSamplerCSC::Params CSCparameters; - Ptr CSCSampler = new TrackerSamplerCSC( CSCparameters ); - - if( !sampler->addTrackerSamplerAlgorithm( CSCSampler ) ) - return false; - - //or add CSC sampler with default parameters - //sampler->addTrackerSamplerAlgorithm( "CSC" ); - @endcode - @note If you use the second method, you must initialize the TrackerSamplerAlgorithm - */ - bool addTrackerSamplerAlgorithm( String trackerSamplerAlgorithmType ); - - /** @overload - @param sampler The TrackerSamplerAlgorithm - */ - bool addTrackerSamplerAlgorithm( Ptr& sampler ); - - private: - std::vector > > samplers; - std::vector samples; - bool blockAddTrackerSampler; - - void clearSamples(); -}; - -/************************************ TrackerModel Base Classes ************************************/ - -/** @brief Abstract base class for TrackerTargetState that represents a possible state of the target. - -See @cite AAM \f$\hat{x}^{i}_{k}\f$ all the states candidates. - -Inherits this class with your Target state, In own implementation you can add scale variation, -width, height, orientation, etc. - */ -class CV_EXPORTS TrackerTargetState -{ - public: - virtual ~TrackerTargetState() - { - } - ; - /** - * \brief Get the position - * \return The position - */ - Point2f getTargetPosition() const; - - /** - * \brief Set the position - * \param position The position - */ - void setTargetPosition( const Point2f& position ); - /** - * \brief Get the width of the target - * \return The width of the target - */ - int getTargetWidth() const; - - /** - * \brief Set the width of the target - * \param width The width of the target - */ - void setTargetWidth( int width ); - /** - * \brief Get the height of the target - * \return The height of the target - */ - int getTargetHeight() const; - - /** - * \brief Set the height of the target - * \param height The height of the target - */ - void setTargetHeight( int height ); - - protected: - Point2f targetPosition; - int targetWidth; - int targetHeight; - -}; - -/** @brief Represents the model of the target at frame \f$k\f$ (all states and scores) - -See @cite AAM The set of the pair \f$\langle \hat{x}^{i}_{k}, C^{i}_{k} \rangle\f$ -@sa TrackerTargetState - */ -typedef std::vector, float> > ConfidenceMap; - -/** @brief Represents the estimate states for all frames - -@cite AAM \f$x_{k}\f$ is the trajectory of the target up to time \f$k\f$ - -@sa TrackerTargetState - */ -typedef std::vector > Trajectory; - -/** @brief Abstract base class for TrackerStateEstimator that estimates the most likely target state. - -See @cite AAM State estimator - -See @cite AMVOT Statistical modeling (Fig. 3), Table III (generative) - IV (discriminative) - V (hybrid) - */ -class CV_EXPORTS TrackerStateEstimator -{ - public: - virtual ~TrackerStateEstimator(); - - /** @brief Estimate the most likely target state, return the estimated state - @param confidenceMaps The overall appearance model as a list of :cConfidenceMap - */ - Ptr estimate( const std::vector& confidenceMaps ); - - /** @brief Update the ConfidenceMap with the scores - @param confidenceMaps The overall appearance model as a list of :cConfidenceMap - */ - void update( std::vector& confidenceMaps ); - - /** @brief Create TrackerStateEstimator by tracker state estimator type - @param trackeStateEstimatorType The TrackerStateEstimator name - - The modes available now: - - - "BOOSTING" -- Boosting-based discriminative appearance models. See @cite AMVOT section 4.4 - - The modes available soon: - - - "SVM" -- SVM-based discriminative appearance models. See @cite AMVOT section 4.5 - */ - static Ptr create( const String& trackeStateEstimatorType ); - - /** @brief Get the name of the specific TrackerStateEstimator - */ - String getClassName() const; - - protected: - - virtual Ptr estimateImpl( const std::vector& confidenceMaps ) = 0; - virtual void updateImpl( std::vector& confidenceMaps ) = 0; - String className; -}; - -/** @brief Abstract class that represents the model of the target. It must be instantiated by specialized -tracker +#include "tracking_internals.hpp" -See @cite AAM Ak - -Inherits this with your TrackerModel - */ -class CV_EXPORTS TrackerModel -{ - public: - - /** - * \brief Constructor - */ - TrackerModel(); - - /** - * \brief Destructor - */ - virtual ~TrackerModel(); - - /** @brief Set TrackerEstimator, return true if the tracker state estimator is added, false otherwise - @param trackerStateEstimator The TrackerStateEstimator - @note You can add only one TrackerStateEstimator - */ - bool setTrackerStateEstimator( Ptr trackerStateEstimator ); - - /** @brief Estimate the most likely target location - - @cite AAM ME, Model Estimation table I - @param responses Features extracted from TrackerFeatureSet - */ - void modelEstimation( const std::vector& responses ); - - /** @brief Update the model - - @cite AAM MU, Model Update table I - */ - void modelUpdate(); - - /** @brief Run the TrackerStateEstimator, return true if is possible to estimate a new state, false otherwise - */ - bool runStateEstimator(); - - /** @brief Set the current TrackerTargetState in the Trajectory - @param lastTargetState The current TrackerTargetState - */ - void setLastTargetState( const Ptr& lastTargetState ); - - /** @brief Get the last TrackerTargetState from Trajectory - */ - Ptr getLastTargetState() const; - - /** @brief Get the list of the ConfidenceMap - */ - const std::vector& getConfidenceMaps() const; - - /** @brief Get the last ConfidenceMap for the current frame - */ - const ConfidenceMap& getLastConfidenceMap() const; - - /** @brief Get the TrackerStateEstimator - */ - Ptr getTrackerStateEstimator() const; - - private: - - void clearCurrentConfidenceMap(); - - protected: - std::vector confidenceMaps; - Ptr stateEstimator; - ConfidenceMap currentConfidenceMap; - Trajectory trajectory; - int maxCMLength; - - virtual void modelEstimationImpl( const std::vector& responses ) = 0; - virtual void modelUpdateImpl() = 0; +namespace cv { +namespace legacy { +#ifndef CV_DOXYGEN +inline namespace tracking { +#endif +using namespace cv::detail::tracking; -}; +/** @addtogroup tracking_legacy +@{ +*/ /************************************ Tracker Base Class ************************************/ @@ -524,7 +72,7 @@ class CV_EXPORTS TrackerModel class CV_EXPORTS_W Tracker : public virtual Algorithm { public: - + Tracker(); virtual ~Tracker() CV_OVERRIDE; /** @brief Initialize the tracker with a known bounding box that surrounded the target @@ -562,496 +110,6 @@ class CV_EXPORTS_W Tracker : public virtual Algorithm }; -/************************************ Specific TrackerStateEstimator Classes ************************************/ - -/** @brief TrackerStateEstimator based on Boosting - */ -class CV_EXPORTS TrackerStateEstimatorMILBoosting : public TrackerStateEstimator -{ - public: - - /** - * Implementation of the target state for TrackerStateEstimatorMILBoosting - */ - class TrackerMILTargetState : public TrackerTargetState - { - - public: - /** - * \brief Constructor - * \param position Top left corner of the bounding box - * \param width Width of the bounding box - * \param height Height of the bounding box - * \param foreground label for target or background - * \param features features extracted - */ - TrackerMILTargetState( const Point2f& position, int width, int height, bool foreground, const Mat& features ); - - /** - * \brief Destructor - */ - ~TrackerMILTargetState() - { - } - ; - - /** @brief Set label: true for target foreground, false for background - @param foreground Label for background/foreground - */ - void setTargetFg( bool foreground ); - /** @brief Set the features extracted from TrackerFeatureSet - @param features The features extracted - */ - void setFeatures( const Mat& features ); - /** @brief Get the label. Return true for target foreground, false for background - */ - bool isTargetFg() const; - /** @brief Get the features extracted - */ - Mat getFeatures() const; - - private: - bool isTarget; - Mat targetFeatures; - }; - - /** @brief Constructor - @param nFeatures Number of features for each sample - */ - TrackerStateEstimatorMILBoosting( int nFeatures = 250 ); - ~TrackerStateEstimatorMILBoosting(); - - /** @brief Set the current confidenceMap - @param confidenceMap The current :cConfidenceMap - */ - void setCurrentConfidenceMap( ConfidenceMap& confidenceMap ); - - protected: - Ptr estimateImpl( const std::vector& confidenceMaps ) CV_OVERRIDE; - void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; - - private: - uint max_idx( const std::vector &v ); - void prepareData( const ConfidenceMap& confidenceMap, Mat& positive, Mat& negative ); - - ClfMilBoost boostMILModel; - bool trained; - int numFeatures; - - ConfidenceMap currentConfidenceMap; -}; - -/** @brief TrackerStateEstimatorAdaBoosting based on ADA-Boosting - */ -class CV_EXPORTS TrackerStateEstimatorAdaBoosting : public TrackerStateEstimator -{ - public: - /** @brief Implementation of the target state for TrackerAdaBoostingTargetState - */ - class TrackerAdaBoostingTargetState : public TrackerTargetState - { - - public: - /** - * \brief Constructor - * \param position Top left corner of the bounding box - * \param width Width of the bounding box - * \param height Height of the bounding box - * \param foreground label for target or background - * \param responses list of features - */ - TrackerAdaBoostingTargetState( const Point2f& position, int width, int height, bool foreground, const Mat& responses ); - - /** - * \brief Destructor - */ - ~TrackerAdaBoostingTargetState() - { - } - ; - - /** @brief Set the features extracted from TrackerFeatureSet - @param responses The features extracted - */ - void setTargetResponses( const Mat& responses ); - /** @brief Set label: true for target foreground, false for background - @param foreground Label for background/foreground - */ - void setTargetFg( bool foreground ); - /** @brief Get the features extracted - */ - Mat getTargetResponses() const; - /** @brief Get the label. Return true for target foreground, false for background - */ - bool isTargetFg() const; - - private: - bool isTarget; - Mat targetResponses; - - }; - - /** @brief Constructor - @param numClassifer Number of base classifiers - @param initIterations Number of iterations in the initialization - @param nFeatures Number of features/weak classifiers - @param patchSize tracking rect - @param ROI initial ROI - */ - TrackerStateEstimatorAdaBoosting( int numClassifer, int initIterations, int nFeatures, Size patchSize, const Rect& ROI ); - - /** - * \brief Destructor - */ - ~TrackerStateEstimatorAdaBoosting(); - - /** @brief Get the sampling ROI - */ - Rect getSampleROI() const; - - /** @brief Set the sampling ROI - @param ROI the sampling ROI - */ - void setSampleROI( const Rect& ROI ); - - /** @brief Set the current confidenceMap - @param confidenceMap The current :cConfidenceMap - */ - void setCurrentConfidenceMap( ConfidenceMap& confidenceMap ); - - /** @brief Get the list of the selected weak classifiers for the classification step - */ - std::vector computeSelectedWeakClassifier(); - - /** @brief Get the list of the weak classifiers that should be replaced - */ - std::vector computeReplacedClassifier(); - - /** @brief Get the list of the weak classifiers that replace those to be replaced - */ - std::vector computeSwappedClassifier(); - - protected: - Ptr estimateImpl( const std::vector& confidenceMaps ) CV_OVERRIDE; - void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; - - Ptr boostClassifier; - - private: - int numBaseClassifier; - int iterationInit; - int numFeatures; - bool trained; - Size initPatchSize; - Rect sampleROI; - std::vector replacedClassifier; - std::vector swappedClassifier; - - ConfidenceMap currentConfidenceMap; -}; - -/** - * \brief TrackerStateEstimator based on SVM - */ -class CV_EXPORTS TrackerStateEstimatorSVM : public TrackerStateEstimator -{ - public: - TrackerStateEstimatorSVM(); - ~TrackerStateEstimatorSVM(); - - protected: - Ptr estimateImpl( const std::vector& confidenceMaps ) CV_OVERRIDE; - void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; -}; - -/************************************ Specific TrackerSamplerAlgorithm Classes ************************************/ - -/** @brief TrackerSampler based on CSC (current state centered), used by MIL algorithm TrackerMIL - */ -class CV_EXPORTS TrackerSamplerCSC : public TrackerSamplerAlgorithm -{ - public: - enum - { - MODE_INIT_POS = 1, //!< mode for init positive samples - MODE_INIT_NEG = 2, //!< mode for init negative samples - MODE_TRACK_POS = 3, //!< mode for update positive samples - MODE_TRACK_NEG = 4, //!< mode for update negative samples - MODE_DETECT = 5 //!< mode for detect samples - }; - - struct CV_EXPORTS Params - { - Params(); - float initInRad; //!< radius for gathering positive instances during init - float trackInPosRad; //!< radius for gathering positive instances during tracking - float searchWinSize; //!< size of search window - int initMaxNegNum; //!< # negative samples to use during init - int trackMaxPosNum; //!< # positive samples to use during training - int trackMaxNegNum; //!< # negative samples to use during training - }; - - /** @brief Constructor - @param parameters TrackerSamplerCSC parameters TrackerSamplerCSC::Params - */ - TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meters = TrackerSamplerCSC::Params() ); - - /** @brief Set the sampling mode of TrackerSamplerCSC - @param samplingMode The sampling mode - - The modes are: - - - "MODE_INIT_POS = 1" -- for the positive sampling in initialization step - - "MODE_INIT_NEG = 2" -- for the negative sampling in initialization step - - "MODE_TRACK_POS = 3" -- for the positive sampling in update step - - "MODE_TRACK_NEG = 4" -- for the negative sampling in update step - - "MODE_DETECT = 5" -- for the sampling in detection step - */ - void setMode( int samplingMode ); - - ~TrackerSamplerCSC(); - - protected: - - bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) CV_OVERRIDE; - - private: - - Params params; - int mode; - RNG rng; - - std::vector sampleImage( const Mat& img, int x, int y, int w, int h, float inrad, float outrad = 0, int maxnum = 1000000 ); -}; - -/** @brief TrackerSampler based on CS (current state), used by algorithm TrackerBoosting - */ -class CV_EXPORTS TrackerSamplerCS : public TrackerSamplerAlgorithm -{ - public: - enum - { - MODE_POSITIVE = 1, //!< mode for positive samples - MODE_NEGATIVE = 2, //!< mode for negative samples - MODE_CLASSIFY = 3 //!< mode for classify samples - }; - - struct CV_EXPORTS Params - { - Params(); - float overlap; //!& sample ) CV_OVERRIDE; - Rect getROI() const; - private: - Rect getTrackingROI( float searchFactor ); - Rect RectMultiply( const Rect & rect, float f ); - std::vector patchesRegularScan( const Mat& image, Rect trackingROI, Size patchSize ); - void setCheckedROI( Rect imageROI ); - - Params params; - int mode; - Rect trackedPatch; - Rect validROI; - Rect ROI; - -}; - -/** @brief This sampler is based on particle filtering. - -In principle, it can be thought of as performing some sort of optimization (and indeed, this -tracker uses opencv's optim module), where tracker seeks to find the rectangle in given frame, -which is the most *"similar"* to the initial rectangle (the one, given through the constructor). - -The optimization performed is stochastic and somehow resembles genetic algorithms, where on each new -image received (submitted via TrackerSamplerPF::sampling()) we start with the region bounded by -boundingBox, then generate several "perturbed" boxes, take the ones most similar to the original. -This selection round is repeated several times. At the end, we hope that only the most promising box -remaining, and these are combined to produce the subrectangle of image, which is put as a sole -element in array sample. - -It should be noted, that the definition of "similarity" between two rectangles is based on comparing -their histograms. As experiments show, tracker is *not* very succesfull if target is assumed to -strongly change its dimensions. - */ -class CV_EXPORTS TrackerSamplerPF : public TrackerSamplerAlgorithm -{ -public: - /** @brief This structure contains all the parameters that can be varied during the course of sampling - algorithm. Below is the structure exposed, together with its members briefly explained with - reference to the above discussion on algorithm's working. - */ - struct CV_EXPORTS Params - { - Params(); - int iterationNum; //!< number of selection rounds - int particlesNum; //!< number of "perturbed" boxes on each round - double alpha; //!< with each new round we exponentially decrease the amount of "perturbing" we allow (like in simulated annealing) - //!< and this very alpha controls how fast annealing happens, ie. how fast perturbing decreases - Mat_ std; //!< initial values for perturbing (1-by-4 array, as each rectangle is given by 4 values -- coordinates of opposite vertices, - //!< hence we have 4 values to perturb) - }; - /** @brief Constructor - @param chosenRect Initial rectangle, that is supposed to contain target we'd like to track. - @param parameters - */ - TrackerSamplerPF(const Mat& chosenRect,const TrackerSamplerPF::Params ¶meters = TrackerSamplerPF::Params()); -protected: - bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) CV_OVERRIDE; -private: - Params params; - Ptr _solver; - Ptr _function; -}; - -/************************************ Specific TrackerFeature Classes ************************************/ - -/** - * \brief TrackerFeature based on Feature2D - */ -class CV_EXPORTS TrackerFeatureFeature2d : public TrackerFeature -{ - public: - - /** - * \brief Constructor - * \param detectorType string of FeatureDetector - * \param descriptorType string of DescriptorExtractor - */ - TrackerFeatureFeature2d( String detectorType, String descriptorType ); - - ~TrackerFeatureFeature2d() CV_OVERRIDE; - - void selection( Mat& response, int npoints ) CV_OVERRIDE; - - protected: - - bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; - - private: - - std::vector keypoints; -}; - -/** - * \brief TrackerFeature based on HOG - */ -class CV_EXPORTS TrackerFeatureHOG : public TrackerFeature -{ - public: - - TrackerFeatureHOG(); - - ~TrackerFeatureHOG() CV_OVERRIDE; - - void selection( Mat& response, int npoints ) CV_OVERRIDE; - - protected: - - bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; - -}; - -/** @brief TrackerFeature based on HAAR features, used by TrackerMIL and many others algorithms -@note HAAR features implementation is copied from apps/traincascade and modified according to MIL - */ -class CV_EXPORTS TrackerFeatureHAAR : public TrackerFeature -{ - public: - struct CV_EXPORTS Params - { - Params(); - int numFeatures; //!< # of rects - Size rectSize; //!< rect size - bool isIntegral; //!< true if input images are integral, false otherwise - }; - - /** @brief Constructor - @param parameters TrackerFeatureHAAR parameters TrackerFeatureHAAR::Params - */ - TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶meters = TrackerFeatureHAAR::Params() ); - - ~TrackerFeatureHAAR() CV_OVERRIDE; - - /** @brief Compute the features only for the selected indices in the images collection - @param selFeatures indices of selected features - @param images The images - @param response Collection of response for the specific TrackerFeature - */ - bool extractSelected( const std::vector selFeatures, const std::vector& images, Mat& response ); - - /** @brief Identify most effective features - @param response Collection of response for the specific TrackerFeature - @param npoints Max number of features - - @note This method modifies the response parameter - */ - void selection( Mat& response, int npoints ) CV_OVERRIDE; - - /** @brief Swap the feature in position source with the feature in position target - @param source The source position - @param target The target position - */ - bool swapFeature( int source, int target ); - - /** @brief Swap the feature in position id with the feature input - @param id The position - @param feature The feature - */ - bool swapFeature( int id, CvHaarEvaluator::FeatureHaar& feature ); - - /** @brief Get the feature in position id - @param id The position - */ - CvHaarEvaluator::FeatureHaar& getFeatureAt( int id ); - - protected: - bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; - - private: - - Params params; - Ptr featureEvaluator; -}; - -/** - * \brief TrackerFeature based on LBP - */ -class CV_EXPORTS TrackerFeatureLBP : public TrackerFeature -{ - public: - - TrackerFeatureLBP(); - - ~TrackerFeatureLBP(); - - void selection( Mat& response, int npoints ) CV_OVERRIDE; - - protected: - - bool computeImpl( const std::vector& images, Mat& response ) CV_OVERRIDE; - -}; - /************************************ Specific Tracker Classes ************************************/ /** @brief The MIL algorithm trains a classifier in an online manner to separate the object from the @@ -1062,21 +120,11 @@ based on @cite MIL . Original code can be found here */ -class CV_EXPORTS_W TrackerMIL : public Tracker +class CV_EXPORTS_W TrackerMIL : public cv::legacy::Tracker { public: - struct CV_EXPORTS Params + struct CV_EXPORTS Params : cv::tracking::TrackerMIL::Params { - Params(); - //parameters for sampler - float samplerInitInRadius; //!< radius for gathering positive instances during init - int samplerInitMaxNegNum; //!< # negative samples to use during init - float samplerSearchWinSize; //!< size of search window - float samplerTrackInRadius; //!< radius for gathering positive instances during tracking - int samplerTrackMaxPosNum; //!< # positive samples to use during tracking - int samplerTrackMaxNegNum; //!< # negative samples to use during tracking - int featureSetNumFeatures; //!< # features - void read( const FileNode& fn ); void write( FileStorage& fs ) const; }; @@ -1084,9 +132,9 @@ class CV_EXPORTS_W TrackerMIL : public Tracker /** @brief Constructor @param parameters MIL parameters TrackerMIL::Params */ - static Ptr create(const TrackerMIL::Params ¶meters); + static Ptr create(const TrackerMIL::Params ¶meters); - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); virtual ~TrackerMIL() CV_OVERRIDE {} }; @@ -1097,7 +145,7 @@ This is a real-time object tracking based on a novel on-line version of the AdaB The classifier uses the surrounding background as negative examples in update step to avoid the drifting problem. The implementation is based on @cite OLB . */ -class CV_EXPORTS_W TrackerBoosting : public Tracker +class CV_EXPORTS_W TrackerBoosting : public cv::legacy::Tracker { public: struct CV_EXPORTS Params @@ -1122,9 +170,9 @@ class CV_EXPORTS_W TrackerBoosting : public Tracker /** @brief Constructor @param parameters BOOSTING parameters TrackerBoosting::Params */ - static Ptr create(const TrackerBoosting::Params ¶meters); + static Ptr create(const TrackerBoosting::Params ¶meters); - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); virtual ~TrackerBoosting() CV_OVERRIDE {} }; @@ -1139,7 +187,7 @@ by authors to outperform MIL). During the implementation period the code at , the courtesy of the author Arthur Amarra, was used for the reference purpose. */ -class CV_EXPORTS_W TrackerMedianFlow : public Tracker +class CV_EXPORTS_W TrackerMedianFlow : public cv::legacy::Tracker { public: struct CV_EXPORTS Params @@ -1161,9 +209,9 @@ class CV_EXPORTS_W TrackerMedianFlow : public Tracker /** @brief Constructor @param parameters Median Flow parameters TrackerMedianFlow::Params */ - static Ptr create(const TrackerMedianFlow::Params ¶meters); + static Ptr create(const TrackerMedianFlow::Params ¶meters); - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); virtual ~TrackerMedianFlow() CV_OVERRIDE {} }; @@ -1181,7 +229,7 @@ The Median Flow algorithm (see cv::TrackerMedianFlow) was chosen as a tracking c implementation, following authors. The tracker is supposed to be able to handle rapid motions, partial occlusions, object absence etc. */ -class CV_EXPORTS_W TrackerTLD : public Tracker +class CV_EXPORTS_W TrackerTLD : public cv::legacy::Tracker { public: struct CV_EXPORTS Params @@ -1194,9 +242,9 @@ class CV_EXPORTS_W TrackerTLD : public Tracker /** @brief Constructor @param parameters TLD parameters TrackerTLD::Params */ - static Ptr create(const TrackerTLD::Params ¶meters); + static Ptr create(const TrackerTLD::Params ¶meters); - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); virtual ~TrackerTLD() CV_OVERRIDE {} }; @@ -1209,7 +257,7 @@ class CV_EXPORTS_W TrackerTLD : public Tracker * as well as the matlab implementation. For more information about KCF with color-names features, please refer to * . */ -class CV_EXPORTS_W TrackerKCF : public Tracker +class CV_EXPORTS_W TrackerKCF : public cv::legacy::Tracker { public: /** @@ -1218,43 +266,12 @@ class CV_EXPORTS_W TrackerKCF : public Tracker - "GRAY" -- Use grayscale values as the feature - "CN" -- Color-names feature */ - enum MODE { - GRAY = (1 << 0), - CN = (1 << 1), - CUSTOM = (1 << 2) - }; + typedef enum cv::tracking::TrackerKCF::MODE MODE; - struct CV_EXPORTS Params + struct CV_EXPORTS Params : cv::tracking::TrackerKCF::Params { - /** - * \brief Constructor - */ - Params(); - - /** - * \brief Read parameters from a file - */ void read(const FileNode& /*fn*/); - - /** - * \brief Write parameters to a file - */ void write(FileStorage& /*fs*/) const; - - float detect_thresh; //!< detection confidence threshold - float sigma; //!< gaussian kernel bandwidth - float lambda; //!< regularization - float interp_factor; //!< linear interpolation factor for adaptation - float output_sigma_factor; //!< spatial bandwidth (proportional to target) - float pca_learning_rate; //!< compression learning rate - bool resize; //!< activate the resize feature to improve the processing speed - bool split_coeff; //!< split the training coefficients into two matrices - bool wrap_kernel; //!< wrap around the kernel values - bool compress_feature; //!< activate the pca method to compress the features - int max_patch_size; //!< threshold for the ROI size - int compressed_size; //!< feature size after compression - int desc_pca; //!< compressed descriptors of TrackerKCF::MODE - int desc_npca; //!< non-compressed descriptors of TrackerKCF::MODE }; virtual void setFeatureExtractor(void(*)(const Mat, const Rect, Mat&), bool pca_func = false) = 0; @@ -1262,13 +279,14 @@ class CV_EXPORTS_W TrackerKCF : public Tracker /** @brief Constructor @param parameters KCF parameters TrackerKCF::Params */ - static Ptr create(const TrackerKCF::Params ¶meters); + static Ptr create(const TrackerKCF::Params ¶meters); - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); virtual ~TrackerKCF() CV_OVERRIDE {} }; +#if 0 // legacy variant is not available /** @brief the GOTURN (Generic Object Tracking Using Regression Networks) tracker * GOTURN (@cite GOTURN) is kind of trackers based on Convolutional Neural Networks (CNN). While taking all advantages of CNN trackers, @@ -1284,7 +302,7 @@ class CV_EXPORTS_W TrackerKCF : public Tracker * * GOTURN architecture goturn.prototxt and trained model goturn.caffemodel are accessible on opencv_extra GitHub repository. */ -class CV_EXPORTS_W TrackerGOTURN : public Tracker +class CV_EXPORTS_W TrackerGOTURN : public cv::legacy::Tracker { public: struct CV_EXPORTS Params @@ -1299,12 +317,13 @@ class CV_EXPORTS_W TrackerGOTURN : public Tracker /** @brief Constructor @param parameters GOTURN parameters TrackerGOTURN::Params */ - static Ptr create(const TrackerGOTURN::Params ¶meters); + static Ptr create(const TrackerGOTURN::Params ¶meters); - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); virtual ~TrackerGOTURN() CV_OVERRIDE {} }; +#endif /** @brief the MOSSE (Minimum Output Sum of Squared %Error) tracker @@ -1312,12 +331,12 @@ The implementation is based on @cite MOSSE Visual Object Tracking using Adaptive @note this tracker works with grayscale images, if passed bgr ones, they will get converted internally. */ -class CV_EXPORTS_W TrackerMOSSE : public Tracker +class CV_EXPORTS_W TrackerMOSSE : public cv::legacy::Tracker { public: /** @brief Constructor */ - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); virtual ~TrackerMOSSE() CV_OVERRIDE {} }; @@ -1350,7 +369,7 @@ class CV_EXPORTS_W MultiTracker : public Algorithm * @param image input image * @param boundingBox a rectangle represents ROI of the tracked object */ - CV_WRAP bool add(Ptr newTracker, InputArray image, const Rect2d& boundingBox); + CV_WRAP bool add(Ptr newTracker, InputArray image, const Rect2d& boundingBox); /** * \brief Add a set of objects to be tracked. @@ -1358,7 +377,7 @@ class CV_EXPORTS_W MultiTracker : public Algorithm * @param image input image * @param boundingBox list of the tracked objects */ - bool add(std::vector > newTrackers, InputArray image, std::vector boundingBox); + bool add(std::vector > newTrackers, InputArray image, std::vector boundingBox); /** * \brief Update the current tracking status. @@ -1415,7 +434,7 @@ class CV_EXPORTS MultiTracker_Alt @return True if new target initialization went succesfully, false otherwise */ - bool addTarget(InputArray image, const Rect2d& boundingBox, Ptr tracker_algorithm); + bool addTarget(InputArray image, const Rect2d& boundingBox, Ptr tracker_algorithm); /** @brief Update all trackers from the tracking-list, find a new most likely bounding boxes for the targets @param image The current frame @@ -1478,16 +497,11 @@ class CV_EXPORTS MultiTrackerTLD : public MultiTracker_Alt The implementation is based on @cite Lukezic_IJCV2018 Discriminative Correlation Filter with Channel and Spatial Reliability */ -class CV_EXPORTS_W TrackerCSRT : public Tracker +class CV_EXPORTS_W TrackerCSRT : public cv::legacy::Tracker { public: - struct CV_EXPORTS Params + struct CV_EXPORTS Params : cv::tracking::TrackerCSRT::Params { - /** - * \brief Constructor - */ - Params(); - /** * \brief Read parameters from a file */ @@ -1497,52 +511,28 @@ class CV_EXPORTS_W TrackerCSRT : public Tracker * \brief Write parameters to a file */ void write(cv::FileStorage& fs) const; - - bool use_hog; - bool use_color_names; - bool use_gray; - bool use_rgb; - bool use_channel_weights; - bool use_segmentation; - - std::string window_function; //!< Window function: "hann", "cheb", "kaiser" - float kaiser_alpha; - float cheb_attenuation; - - float template_size; - float gsl_sigma; - float hog_orientations; - float hog_clip; - float padding; - float filter_lr; - float weights_lr; - int num_hog_channels_used; - int admm_iterations; - int histogram_bins; - float histogram_lr; - int background_ratio; - int number_of_scales; - float scale_sigma_factor; - float scale_model_max_area; - float scale_lr; - float scale_step; - - float psr_threshold; //!< we lost the target, if the psr is lower than this. }; /** @brief Constructor @param parameters CSRT parameters TrackerCSRT::Params */ - static Ptr create(const TrackerCSRT::Params ¶meters); + static Ptr create(const TrackerCSRT::Params ¶meters); - CV_WRAP static Ptr create(); + CV_WRAP static Ptr create(); CV_WRAP virtual void setInitialMask(InputArray mask) = 0; virtual ~TrackerCSRT() CV_OVERRIDE {} }; + +CV_EXPORTS_W Ptr upgradeTrackingAPI(const Ptr& legacy_tracker); + //! @} -} /* namespace cv */ +#ifndef CV_DOXYGEN +} // namespace #endif +}} // namespace + +#endif // OPENCV_TRACKING_LEGACY_HPP diff --git a/modules/tracking/misc/java/gen_dict.json b/modules/tracking/misc/java/gen_dict.json new file mode 100644 index 00000000000..a2c3bf0dacf --- /dev/null +++ b/modules/tracking/misc/java/gen_dict.json @@ -0,0 +1,5 @@ +{ + "namespaces_dict": { + "cv.legacy": "legacy" + } +} diff --git a/modules/tracking/misc/java/test/TrackerCreateLegacyTest.java b/modules/tracking/misc/java/test/TrackerCreateLegacyTest.java new file mode 100644 index 00000000000..3c8bfa9991e --- /dev/null +++ b/modules/tracking/misc/java/test/TrackerCreateLegacyTest.java @@ -0,0 +1,23 @@ +package org.opencv.test.tracking; + +import org.opencv.core.Core; +import org.opencv.core.CvException; +import org.opencv.test.OpenCVTestCase; + +import org.opencv.tracking.Tracking; +import org.opencv.tracking.legacy_Tracker; +import org.opencv.tracking.legacy_TrackerTLD; + +public class TrackerCreateLegacyTest extends OpenCVTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + + public void testCreateLegacyTrackerTLD() { + legacy_Tracker tracker = legacy_TrackerTLD.create(); + } + +} diff --git a/modules/tracking/misc/java/test/TrackerCreateTest.java b/modules/tracking/misc/java/test/TrackerCreateTest.java new file mode 100644 index 00000000000..189ccfcd5e7 --- /dev/null +++ b/modules/tracking/misc/java/test/TrackerCreateTest.java @@ -0,0 +1,38 @@ +package org.opencv.test.tracking; + +import org.opencv.core.Core; +import org.opencv.core.CvException; +import org.opencv.test.OpenCVTestCase; + +import org.opencv.tracking.Tracking; +import org.opencv.tracking.Tracker; +import org.opencv.tracking.TrackerGOTURN; +import org.opencv.tracking.TrackerKCF; +import org.opencv.tracking.TrackerMIL; + +public class TrackerCreateTest extends OpenCVTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + + public void testCreateTrackerGOTURN() { + try { + Tracker tracker = TrackerGOTURN.create(); + assert(tracker != null); + } catch (CvException e) { + // expected, model files may be missing + } + } + + public void testCreateTrackerKCF() { + Tracker tracker = TrackerKCF.create(); + } + + public void testCreateTrackerMIL() { + Tracker tracker = TrackerMIL.create(); + } + +} diff --git a/modules/tracking/misc/objc/gen_dict.json b/modules/tracking/misc/objc/gen_dict.json index 6cefc5f55ad..7172e2df474 100644 --- a/modules/tracking/misc/objc/gen_dict.json +++ b/modules/tracking/misc/objc/gen_dict.json @@ -4,5 +4,8 @@ }, "AdditionalImports" : { "*" : [ "\"tracking.hpp\"" ] - } + }, + "namespace_ignore_list" : [ + "cv.legacy" + ] } diff --git a/modules/tracking/misc/python/pyopencv_tracking.hpp b/modules/tracking/misc/python/pyopencv_tracking.hpp new file mode 100644 index 00000000000..dd4d5269628 --- /dev/null +++ b/modules/tracking/misc/python/pyopencv_tracking.hpp @@ -0,0 +1,6 @@ +#ifdef HAVE_OPENCV_TRACKING +typedef TrackerCSRT::Params TrackerCSRT_Params; +typedef TrackerKCF::Params TrackerKCF_Params; +typedef TrackerMIL::Params TrackerMIL_Params; +typedef TrackerGOTURN::Params TrackerGOTURN_Params; +#endif diff --git a/modules/tracking/misc/python/test/test_tracking_contrib.py b/modules/tracking/misc/python/test/test_tracking_contrib.py new file mode 100644 index 00000000000..7eeb91e1e36 --- /dev/null +++ b/modules/tracking/misc/python/test/test_tracking_contrib.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +import os +import numpy as np +import cv2 as cv + +from tests_common import NewOpenCVTests, unittest + +class tracking_contrib_test(NewOpenCVTests): + + def test_createTracker(self): + + t = cv.TrackerMIL_create() + t = cv.TrackerKCF_create() + try: + t = cv.TrackerGOTURN_create() + except cv.error as e: + pass # may fail due to missing DL model files + + def test_createLegacyTracker(self): + + t = cv.legacy.TrackerBoosting_create() + t = cv.legacy.TrackerMIL_create() + t = cv.legacy.TrackerKCF_create() + t = cv.legacy.TrackerMedianFlow_create() + #t = cv.legacy.TrackerGOTURN_create() + t = cv.legacy.TrackerMOSSE_create() + t = cv.legacy.TrackerCSRT_create() + + +if __name__ == '__main__': + NewOpenCVTests.bootstrap() diff --git a/modules/tracking/perf/perf_Tracker.cpp b/modules/tracking/perf/perf_Tracker.cpp deleted file mode 100644 index 4667c713b25..00000000000 --- a/modules/tracking/perf/perf_Tracker.cpp +++ /dev/null @@ -1,412 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#include "perf_precomp.hpp" - -namespace opencv_test { namespace { - -//write sanity: ./bin/opencv_perf_tracking --perf_write_sanity=true --perf_min_samples=1 -//verify sanity: ./bin/opencv_perf_tracking --perf_min_samples=1 - -#define TESTSET_NAMES testing::Values("david","dudek","faceocc2") -//#define TESTSET_NAMES testing::internal::ValueArray1("david") -#define SEGMENTS testing::Values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - -const string TRACKING_DIR = "cv/tracking"; -const string FOLDER_IMG = "data"; - -typedef perf::TestBaseWithParam > tracking; - -std::vector splitString( std::string s, std::string delimiter ) -{ - std::vector token; - size_t pos = 0; - while ( ( pos = s.find( delimiter ) ) != std::string::npos ) - { - token.push_back( s.substr( 0, pos ) ); - s.erase( 0, pos + delimiter.length() ); - } - token.push_back( s ); - return token; -} - -void checkData( const string& datasetMeta, int& startFrame, string& prefix, string& suffix ) -{ - //get informations on the current test data - FileStorage fs; - fs.open( datasetMeta, FileStorage::READ ); - fs["start"] >> startFrame; - fs["prefix"] >> prefix; - fs["suffix"] >> suffix; - fs.release(); -} - -bool getGroundTruth( const string& gtFile, vector& gtBBs ) -{ - std::ifstream gt; - //open the ground truth - gt.open( gtFile.c_str() ); - if( !gt.is_open() ) - { - return false; - } - string line; - Rect currentBB; - while ( getline( gt, line ) ) - { - vector tokens = splitString( line, "," ); - - if( tokens.size() != 4 ) - { - return false; - } - - gtBBs.push_back( - Rect( atoi( tokens.at( 0 ).c_str() ), atoi( tokens.at( 1 ).c_str() ), atoi( tokens.at( 2 ).c_str() ), atoi( tokens.at( 3 ).c_str() ) ) ); - } - return true; -} - -void getSegment( int segmentId, int numSegments, int bbCounter, int& startFrame, int& endFrame ) -{ - //compute the start and the and for each segment - int gtStartFrame = startFrame; - int numFrame = bbCounter / numSegments; - startFrame += ( segmentId - 1 ) * numFrame; - endFrame = startFrame + numFrame; - - if( ( segmentId ) == numSegments ) - endFrame = bbCounter + gtStartFrame - 1; -} - -void getMatOfRects( const vector& bbs, Mat& bbs_mat ) -{ - for ( int b = 0, size = (int)bbs.size(); b < size; b++ ) - { - bbs_mat.at( b, 0 ) = (float)bbs[b].x; - bbs_mat.at( b, 1 ) = (float)bbs[b].y; - bbs_mat.at( b, 2 ) = (float)bbs[b].width; - bbs_mat.at( b, 3 ) = (float)bbs[b].height; - } -} - -PERF_TEST_P(tracking, mil, testing::Combine(TESTSET_NAMES, SEGMENTS)) -{ - string video = get<0>( GetParam() ); - int segmentId = get<1>( GetParam() ); - - int startFrame; - string prefix; - string suffix; - string datasetMeta = getDataPath( TRACKING_DIR + "/" + video + "/" + video + ".yml" ); - checkData( datasetMeta, startFrame, prefix, suffix ); - int gtStartFrame = startFrame; - - vector gtBBs; - string gtFile = getDataPath( TRACKING_DIR + "/" + video + "/gt.txt" ); - if( !getGroundTruth( gtFile, gtBBs ) ) - FAIL()<< "Ground truth file " << gtFile << " can not be read" << endl; - int bbCounter = (int)gtBBs.size(); - - Mat frame; - bool initialized = false; - vector bbs; - - Ptr tracker = TrackerMIL::create(); - string folder = TRACKING_DIR + "/" + video + "/" + FOLDER_IMG; - int numSegments = ( sizeof ( SEGMENTS)/sizeof(int) ); - int endFrame = 0; - getSegment( segmentId, numSegments, bbCounter, startFrame, endFrame ); - - Rect currentBBi = gtBBs[startFrame - gtStartFrame]; - Rect2d currentBB(currentBBi); - - TEST_CYCLE_N(1) - { - VideoCapture c; - c.open( getDataPath( TRACKING_DIR + "/" + video + "/" + FOLDER_IMG + "/" + video + ".webm" ) ); - c.set( CAP_PROP_POS_FRAMES, startFrame ); - - for ( int frameCounter = startFrame; frameCounter < endFrame; frameCounter++ ) - { - c >> frame; - - if( frame.empty() ) - { - break; - } - - if( !initialized ) - { - if( !tracker->init( frame, currentBB ) ) - { - FAIL()<< "Could not initialize tracker" << endl; - return; - } - initialized = true; - } - else if( initialized ) - { - tracker->update( frame, currentBB ); - } - bbs.push_back( currentBB ); - - } - } - //save the bounding boxes in a Mat - Mat bbs_mat( (int)bbs.size(), 4, CV_32F ); - getMatOfRects( bbs, bbs_mat ); - - SANITY_CHECK( bbs_mat, 15, ERROR_RELATIVE ); - -} - -PERF_TEST_P(tracking, boosting, testing::Combine(TESTSET_NAMES, SEGMENTS)) -{ - string video = get<0>( GetParam() ); - int segmentId = get<1>( GetParam() ); - - int startFrame; - string prefix; - string suffix; - string datasetMeta = getDataPath( TRACKING_DIR + "/" + video + "/" + video + ".yml" ); - checkData( datasetMeta, startFrame, prefix, suffix ); - int gtStartFrame = startFrame; - - vector gtBBs; - string gtFile = getDataPath( TRACKING_DIR + "/" + video + "/gt.txt" ); - if( !getGroundTruth( gtFile, gtBBs ) ) - FAIL()<< "Ground truth file " << gtFile << " can not be read" << endl; - int bbCounter = (int)gtBBs.size(); - - Mat frame; - bool initialized = false; - vector bbs; - - Ptr tracker = TrackerBoosting::create(); - string folder = TRACKING_DIR + "/" + video + "/" + FOLDER_IMG; - int numSegments = ( sizeof ( SEGMENTS)/sizeof(int) ); - int endFrame = 0; - getSegment( segmentId, numSegments, bbCounter, startFrame, endFrame ); - - Rect currentBBi = gtBBs[startFrame - gtStartFrame]; - Rect2d currentBB(currentBBi); - - TEST_CYCLE_N(1) - { - VideoCapture c; - c.open( getDataPath( TRACKING_DIR + "/" + video + "/" + FOLDER_IMG + "/" + video + ".webm" ) ); - c.set( CAP_PROP_POS_FRAMES, startFrame ); - for ( int frameCounter = startFrame; frameCounter < endFrame; frameCounter++ ) - { - c >> frame; - - if( frame.empty() ) - { - break; - } - - if( !initialized ) - { - if( !tracker->init( frame, currentBB ) ) - { - FAIL()<< "Could not initialize tracker" << endl; - return; - } - initialized = true; - } - else if( initialized ) - { - tracker->update( frame, currentBB ); - } - bbs.push_back( currentBB ); - - } - } - //save the bounding boxes in a Mat - Mat bbs_mat( (int)bbs.size(), 4, CV_32F ); - getMatOfRects( bbs, bbs_mat ); - - SANITY_CHECK( bbs_mat, 15, ERROR_RELATIVE ); - -} - -PERF_TEST_P(tracking, tld, testing::Combine(TESTSET_NAMES, SEGMENTS)) -{ - string video = get<0>( GetParam() ); - int segmentId = get<1>( GetParam() ); - - int startFrame; - string prefix; - string suffix; - string datasetMeta = getDataPath( TRACKING_DIR + "/" + video + "/" + video + ".yml" ); - checkData( datasetMeta, startFrame, prefix, suffix ); - int gtStartFrame = startFrame; - - vector gtBBs; - string gtFile = getDataPath( TRACKING_DIR + "/" + video + "/gt.txt" ); - if( !getGroundTruth( gtFile, gtBBs ) ) - FAIL()<< "Ground truth file " << gtFile << " can not be read" << endl; - int bbCounter = (int)gtBBs.size(); - - Mat frame; - bool initialized = false; - vector bbs; - - Ptr tracker = TrackerTLD::create(); - string folder = TRACKING_DIR + "/" + video + "/" + FOLDER_IMG; - int numSegments = ( sizeof ( SEGMENTS)/sizeof(int) ); - int endFrame = 0; - getSegment( segmentId, numSegments, bbCounter, startFrame, endFrame ); - - Rect currentBBi = gtBBs[startFrame - gtStartFrame]; - Rect2d currentBB(currentBBi); - - TEST_CYCLE_N(1) - { - VideoCapture c; - c.open( getDataPath( TRACKING_DIR + "/" + video + "/" + FOLDER_IMG + "/" + video + ".webm" ) ); - c.set( CAP_PROP_POS_FRAMES, startFrame ); - for ( int frameCounter = startFrame; frameCounter < endFrame; frameCounter++ ) - { - c >> frame; - - if( frame.empty() ) - { - break; - } - - if( !initialized ) - { - if( !tracker->init( frame, currentBB ) ) - { - FAIL()<< "Could not initialize tracker" << endl; - return; - } - initialized = true; - } - else if( initialized ) - { - tracker->update( frame, currentBB ); - } - bbs.push_back( currentBB ); - - } - } - //save the bounding boxes in a Mat - Mat bbs_mat( (int)bbs.size(), 4, CV_32F ); - getMatOfRects( bbs, bbs_mat ); - - SANITY_CHECK( bbs_mat, 15, ERROR_RELATIVE ); - -} - -PERF_TEST_P(tracking, GOTURN, testing::Combine(TESTSET_NAMES, SEGMENTS)) -{ - string video = get<0>(GetParam()); - int segmentId = get<1>(GetParam()); - - int startFrame; - string prefix; - string suffix; - string datasetMeta = getDataPath(TRACKING_DIR + "/" + video + "/" + video + ".yml"); - checkData(datasetMeta, startFrame, prefix, suffix); - int gtStartFrame = startFrame; - - vector gtBBs; - string gtFile = getDataPath(TRACKING_DIR + "/" + video + "/gt.txt"); - if (!getGroundTruth(gtFile, gtBBs)) - FAIL() << "Ground truth file " << gtFile << " can not be read" << endl; - int bbCounter = (int)gtBBs.size(); - - Mat frame; - bool initialized = false; - vector bbs; - - Ptr tracker = TrackerGOTURN::create(); - string folder = TRACKING_DIR + "/" + video + "/" + FOLDER_IMG; - int numSegments = (sizeof(SEGMENTS) / sizeof(int)); - int endFrame = 0; - getSegment(segmentId, numSegments, bbCounter, startFrame, endFrame); - - Rect currentBBi = gtBBs[startFrame - gtStartFrame]; - Rect2d currentBB(currentBBi); - - TEST_CYCLE_N(1) - { - VideoCapture c; - c.open(getDataPath(TRACKING_DIR + "/" + video + "/" + FOLDER_IMG + "/" + video + ".webm")); - c.set(CAP_PROP_POS_FRAMES, startFrame); - for (int frameCounter = startFrame; frameCounter < endFrame; frameCounter++) - { - c >> frame; - - if (frame.empty()) - { - break; - } - - if (!initialized) - { - if (!tracker->init(frame, currentBB)) - { - FAIL() << "Could not initialize tracker" << endl; - return; - } - initialized = true; - } - else if (initialized) - { - tracker->update(frame, currentBB); - } - bbs.push_back(currentBB); - - } - } - //save the bounding boxes in a Mat - Mat bbs_mat((int)bbs.size(), 4, CV_32F); - getMatOfRects(bbs, bbs_mat); - - SANITY_CHECK(bbs_mat, 15, ERROR_RELATIVE); - -} - -}} // namespace diff --git a/modules/tracking/perf/perf_main.cpp b/modules/tracking/perf/perf_main.cpp index cfd91a4dc7a..ccca051e660 100644 --- a/modules/tracking/perf/perf_main.cpp +++ b/modules/tracking/perf/perf_main.cpp @@ -3,4 +3,19 @@ // of this distribution and at http://opencv.org/license.html. #include "perf_precomp.hpp" -CV_PERF_TEST_MAIN(tracking) +static +void initTrackingTests() +{ + const char* extraTestDataPath = +#ifdef WINRT + NULL; +#else + getenv("OPENCV_DNN_TEST_DATA_PATH"); +#endif + if (extraTestDataPath) + cvtest::addDataSearchPath(extraTestDataPath); + + cvtest::addDataSearchSubDirectory(""); // override "cv" prefix below to access without "../dnn" hacks +} + +CV_PERF_TEST_MAIN(tracking, initTrackingTests()) diff --git a/modules/tracking/perf/perf_trackers.cpp b/modules/tracking/perf/perf_trackers.cpp new file mode 100644 index 00000000000..e76b15b2c02 --- /dev/null +++ b/modules/tracking/perf/perf_trackers.cpp @@ -0,0 +1,119 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "perf_precomp.hpp" + +#include + +namespace opencv_test { namespace { +using namespace perf; + +//using namespace cv::tracking; + +typedef tuple TrackingParams_t; + +std::vector getTrackingParams() +{ + std::vector params { + TrackingParams_t("david/data/david.webm", 300, Rect(163,62,47,56)), + TrackingParams_t("dudek/data/dudek.webm", 1, Rect(123,87,132,176)), + TrackingParams_t("faceocc2/data/faceocc2.webm", 1, Rect(118,57,82,98)) + }; + return params; +} + +class Tracking : public perf::TestBaseWithParam +{ +public: + template + void runTrackingTest(const Ptr& tracker, const TrackingParams_t& params); +}; + +template +void Tracking::runTrackingTest(const Ptr& tracker, const TrackingParams_t& params) +{ + const int N = 10; + string video = get<0>(params); + int startFrame = get<1>(params); + //int endFrame = startFrame + N; + Rect boundingBox = get<2>(params); + + string videoPath = findDataFile(std::string("cv/tracking/") + video); + + VideoCapture c; + c.open(videoPath); + ASSERT_TRUE(c.isOpened()) << videoPath; +#if 0 + // c.set(CAP_PROP_POS_FRAMES, startFrame); +#else + if (startFrame) + std::cout << "startFrame = " << startFrame << std::endl; + for (int i = 0; i < startFrame; i++) + { + Mat dummy_frame; + c >> dummy_frame; + ASSERT_FALSE(dummy_frame.empty()) << i << ": " << videoPath; + } +#endif + + // decode frames into memory (don't measure decoding performance) + std::vector frames; + for (int i = 0; i < N; ++i) + { + Mat frame; + c >> frame; + ASSERT_FALSE(frame.empty()) << "i=" << i; + frames.push_back(frame); + } + + std::cout << "frame size = " << frames[0].size() << std::endl; + + PERF_SAMPLE_BEGIN(); + { + tracker->init(frames[0], (ROI_t)boundingBox); + for (int i = 1; i < N; ++i) + { + ROI_t rc; + tracker->update(frames[i], rc); + ASSERT_FALSE(rc.empty()); + } + } + PERF_SAMPLE_END(); + + SANITY_CHECK_NOTHING(); +} + + +//================================================================================================== + +PERF_TEST_P(Tracking, MIL, testing::ValuesIn(getTrackingParams())) +{ + auto tracker = TrackerMIL::create(); + runTrackingTest(tracker, GetParam()); +} + +PERF_TEST_P(Tracking, Boosting, testing::ValuesIn(getTrackingParams())) +{ + auto tracker = legacy::TrackerBoosting::create(); + runTrackingTest(tracker, GetParam()); +} + +PERF_TEST_P(Tracking, TLD, testing::ValuesIn(getTrackingParams())) +{ + auto tracker = legacy::TrackerTLD::create(); + runTrackingTest(tracker, GetParam()); +} + +PERF_TEST_P(Tracking, GOTURN, testing::ValuesIn(getTrackingParams())) +{ + std::string model = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.prototxt"); + std::string weights = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.caffemodel", false); + TrackerGOTURN::Params params; + params.modelTxt = model; + params.modelBin = weights; + auto tracker = TrackerGOTURN::create(params); + runTrackingTest(tracker, GetParam()); +} + +}} // namespace diff --git a/modules/tracking/samples/benchmark.cpp b/modules/tracking/samples/benchmark.cpp index 7becc6a03e5..935244f92b5 100644 --- a/modules/tracking/samples/benchmark.cpp +++ b/modules/tracking/samples/benchmark.cpp @@ -93,7 +93,7 @@ struct AlgoWrap Ptr tracker; bool lastRes; - Rect2d lastBox; + Rect lastBox; State lastState; // visual @@ -112,14 +112,14 @@ struct AlgoWrap void eval(const Mat &frame, const Rect2d >Box, bool isVerbose) { // RUN - lastBox = Rect2d(); + lastBox = Rect(); int64 frameTime = getTickCount(); lastRes = tracker->update(frame, lastBox); frameTime = getTickCount() - frameTime; // RESULTS - double intersectArea = (gtBox & lastBox).area(); - double unionArea = (gtBox | lastBox).area(); + double intersectArea = (gtBox & (Rect2d)lastBox).area(); + double unionArea = (gtBox | (Rect2d)lastBox).area(); numTotal++; numResponse += (lastRes && isGoodBox(lastBox)) ? 1 : 0; numPresent += isGoodBox(gtBox) ? 1 : 0; diff --git a/modules/tracking/samples/csrt.cpp b/modules/tracking/samples/csrt.cpp index 1f77e89a8c5..b52c8d37ac8 100644 --- a/modules/tracking/samples/csrt.cpp +++ b/modules/tracking/samples/csrt.cpp @@ -41,7 +41,7 @@ int main(int argc, char** argv) cap >> frame; // target bounding box - Rect2d roi; + Rect roi; if (argc > 2) { // read first line of ground-truth file std::string groundtruthPath = argv[2]; diff --git a/modules/tracking/samples/goturnTracker.cpp b/modules/tracking/samples/goturnTracker.cpp index a1e72171871..9bdaa97a129 100644 --- a/modules/tracking/samples/goturnTracker.cpp +++ b/modules/tracking/samples/goturnTracker.cpp @@ -50,6 +50,7 @@ #include "opencv2/datasets/track_alov.hpp" #include +#include #include #include #include @@ -65,7 +66,7 @@ static Mat image; static bool paused; static bool selectObjects = false; static bool startSelection = false; -Rect2d boundingBox; +static Rect boundingBox; static const char* keys = { "{@dataset_path || Dataset path }" @@ -140,7 +141,7 @@ int main(int argc, char *argv[]) setMouseCallback("GOTURN Tracking", onMouse, 0); //Create GOTURN tracker - Ptr tracker = TrackerGOTURN::create(); + auto tracker = TrackerGOTURN::create(); //Load and init full ALOV300++ dataset with a given datasetID, as alternative you can use loadAnnotatedOnly(..) //to load only frames with labelled ground truth ~ every 5-th frame @@ -181,11 +182,7 @@ int main(int argc, char *argv[]) if (!initialized && selectObjects) { //Initialize the tracker and add targets - if (!tracker->init(frame, boundingBox)) - { - cout << "Tracker Init Error!!!"; - return 0; - } + tracker->init(frame, boundingBox); rectangle(frame, boundingBox, Scalar(0, 0, 255), 2, 1); initialized = true; } diff --git a/modules/tracking/samples/kcf.cpp b/modules/tracking/samples/kcf.cpp index f40a3c5f266..e1d7ab20745 100644 --- a/modules/tracking/samples/kcf.cpp +++ b/modules/tracking/samples/kcf.cpp @@ -41,7 +41,7 @@ int main( int argc, char** argv ){ // get bounding box cap >> frame; - Rect2d roi= selectROI("tracker", frame, true, false); + Rect roi = selectROI("tracker", frame, true, false); //quit if ROI was not selected if(roi.width==0 || roi.height==0) diff --git a/modules/tracking/samples/multiTracker_dataset.cpp b/modules/tracking/samples/multiTracker_dataset.cpp index 622a69ac758..efaed5633c1 100644 --- a/modules/tracking/samples/multiTracker_dataset.cpp +++ b/modules/tracking/samples/multiTracker_dataset.cpp @@ -148,7 +148,7 @@ int main(int argc, char *argv[]) namedWindow("Tracking API", 0); setMouseCallback("Tracking API", onMouse, 0); - MultiTrackerTLD mt; + legacy::MultiTrackerTLD mt; //Init Dataset Ptr dataset = TRACK_vot::create(); dataset->load(datasetRootPath); @@ -185,7 +185,7 @@ int main(int argc, char *argv[]) //Initialize the tracker and add targets for (int i = 0; i < (int)boundingBoxes.size(); i++) { - if (!mt.addTarget(frame, boundingBoxes[i], createTrackerByName(tracker_algorithm))) + if (!mt.addTarget(frame, boundingBoxes[i], createTrackerByName_legacy(tracker_algorithm))) { cout << "Trackers Init Error!!!"; return 0; diff --git a/modules/tracking/samples/multitracker.cpp b/modules/tracking/samples/multitracker.cpp index 7b21334d503..62df0199192 100644 --- a/modules/tracking/samples/multitracker.cpp +++ b/modules/tracking/samples/multitracker.cpp @@ -73,7 +73,7 @@ int main( int argc, char** argv ){ trackingAlg = argv[2]; // create the tracker - MultiTracker trackers; + legacy::MultiTracker trackers; // container of the tracked objects vector ROIs; @@ -93,10 +93,10 @@ int main( int argc, char** argv ){ if(ROIs.size()<1) return 0; - std::vector > algorithms; + std::vector > algorithms; for (size_t i = 0; i < ROIs.size(); i++) { - algorithms.push_back(createTrackerByName(trackingAlg)); + algorithms.push_back(createTrackerByName_legacy(trackingAlg)); objects.push_back(ROIs[i]); } diff --git a/modules/tracking/samples/samples_utility.hpp b/modules/tracking/samples/samples_utility.hpp index de3525d507c..2fe876919ec 100644 --- a/modules/tracking/samples/samples_utility.hpp +++ b/modules/tracking/samples/samples_utility.hpp @@ -2,25 +2,28 @@ #define _SAMPLES_UTILITY_HPP_ #include +#include -inline cv::Ptr createTrackerByName(cv::String name) +inline cv::Ptr createTrackerByName(const std::string& name) { + using namespace cv; + cv::Ptr tracker; if (name == "KCF") tracker = cv::TrackerKCF::create(); else if (name == "TLD") - tracker = cv::TrackerTLD::create(); + tracker = legacy::upgradeTrackingAPI(legacy::TrackerTLD::create()); else if (name == "BOOSTING") - tracker = cv::TrackerBoosting::create(); + tracker = legacy::upgradeTrackingAPI(legacy::TrackerBoosting::create()); else if (name == "MEDIAN_FLOW") - tracker = cv::TrackerMedianFlow::create(); + tracker = legacy::upgradeTrackingAPI(legacy::TrackerMedianFlow::create()); else if (name == "MIL") tracker = cv::TrackerMIL::create(); else if (name == "GOTURN") tracker = cv::TrackerGOTURN::create(); else if (name == "MOSSE") - tracker = cv::TrackerMOSSE::create(); + tracker = legacy::upgradeTrackingAPI(legacy::TrackerMOSSE::create()); else if (name == "CSRT") tracker = cv::TrackerCSRT::create(); else @@ -29,4 +32,32 @@ inline cv::Ptr createTrackerByName(cv::String name) return tracker; } +inline cv::Ptr createTrackerByName_legacy(const std::string& name) +{ + using namespace cv; + + cv::Ptr tracker; + + if (name == "KCF") + tracker = legacy::TrackerKCF::create(); + else if (name == "TLD") + tracker = legacy::TrackerTLD::create(); + else if (name == "BOOSTING") + tracker = legacy::TrackerBoosting::create(); + else if (name == "MEDIAN_FLOW") + tracker = legacy::TrackerMedianFlow::create(); + else if (name == "MIL") + tracker = legacy::TrackerMIL::create(); + else if (name == "GOTURN") + CV_Error(cv::Error::StsNotImplemented, "FIXIT: migration on new API is required"); + else if (name == "MOSSE") + tracker = legacy::TrackerMOSSE::create(); + else if (name == "CSRT") + tracker = legacy::TrackerCSRT::create(); + else + CV_Error(cv::Error::StsBadArg, "Invalid tracking algorithm name\n"); + + return tracker; +} + #endif diff --git a/modules/tracking/samples/tracker.cpp b/modules/tracking/samples/tracker.cpp index add014c8a1c..5de5e60f616 100644 --- a/modules/tracking/samples/tracker.cpp +++ b/modules/tracking/samples/tracker.cpp @@ -88,7 +88,7 @@ int main( int argc, char** argv ){ namedWindow( "Tracking API", 1 ); Mat image; - Rect2d boundingBox; + Rect boundingBox; bool paused = false; //instantiates the specific Tracker @@ -134,11 +134,7 @@ int main( int argc, char** argv ){ if( !initialized ) { //initializes the tracker - if( !tracker->init( frame, boundingBox ) ) - { - cout << "***Could not initialize tracker...***\n"; - return -1; - } + tracker->init(frame, boundingBox); initialized = true; } else if( initialized ) diff --git a/modules/tracking/samples/tracker_dataset.cpp b/modules/tracking/samples/tracker_dataset.cpp index 1dbeb4c8407..479546bb77d 100644 --- a/modules/tracking/samples/tracker_dataset.cpp +++ b/modules/tracking/samples/tracker_dataset.cpp @@ -66,7 +66,7 @@ using namespace cv::datasets; //#define RECORD_VIDEO_FLG static Mat image; -static Rect2d boundingBox; +static Rect boundingBox; static bool paused; static bool selectObject = false; static bool startSelection = false; @@ -186,11 +186,7 @@ int main(int argc, char *argv[]) if (!initialized && selectObject) { //initializes the tracker - if (!tracker->init(frame, boundingBox)) - { - cout << "***Could not initialize tracker...***\n"; - return -1; - } + tracker->init(frame, boundingBox); initialized = true; } else if (initialized) diff --git a/modules/tracking/samples/tracking_by_matching.cpp b/modules/tracking/samples/tracking_by_matching.cpp index 1e53845a1e5..d2e10afd683 100644 --- a/modules/tracking/samples/tracking_by_matching.cpp +++ b/modules/tracking/samples/tracking_by_matching.cpp @@ -8,7 +8,8 @@ using namespace std; using namespace cv; -using namespace cv::tbm; +using namespace cv::detail::tracking; +using namespace cv::detail::tracking::tbm; static const char* keys = { "{video_name | | video name }" @@ -123,7 +124,7 @@ class DnnObjectDetector cv::Ptr createTrackerByMatchingWithFastDescriptor() { - cv::tbm::TrackerParams params; + tbm::TrackerParams params; cv::Ptr tracker = createTrackerByMatching(params); diff --git a/modules/tracking/samples/tutorial_customizing_cn_tracker.cpp b/modules/tracking/samples/tutorial_customizing_cn_tracker.cpp index cd4418b2d2a..58e2beb03dd 100644 --- a/modules/tracking/samples/tutorial_customizing_cn_tracker.cpp +++ b/modules/tracking/samples/tutorial_customizing_cn_tracker.cpp @@ -25,7 +25,7 @@ int main( int argc, char** argv ){ } // declares all required variables - Rect2d roi; + Rect roi; Mat frame; //! [param] diff --git a/modules/tracking/samples/tutorial_introduction_to_tracker.cpp b/modules/tracking/samples/tutorial_introduction_to_tracker.cpp index e092c7d9946..4083c32d3df 100644 --- a/modules/tracking/samples/tutorial_introduction_to_tracker.cpp +++ b/modules/tracking/samples/tutorial_introduction_to_tracker.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -24,7 +25,7 @@ int main( int argc, char** argv ){ // declares all required variables //! [vars] - Rect2d roi; + Rect roi; Mat frame; //! [vars] diff --git a/modules/tracking/samples/tutorial_multitracker.cpp b/modules/tracking/samples/tutorial_multitracker.cpp index 0c6d218006b..efb9af881f6 100644 --- a/modules/tracking/samples/tutorial_multitracker.cpp +++ b/modules/tracking/samples/tutorial_multitracker.cpp @@ -40,7 +40,7 @@ int main( int argc, char** argv ){ // create the tracker //! [create] - MultiTracker trackers; + legacy::MultiTracker trackers; //! [create] // container of the tracked objects @@ -67,10 +67,10 @@ int main( int argc, char** argv ){ // initialize the tracker //! [init] - std::vector > algorithms; + std::vector > algorithms; for (size_t i = 0; i < ROIs.size(); i++) { - algorithms.push_back(createTrackerByName(trackingAlg)); + algorithms.push_back(createTrackerByName_legacy(trackingAlg)); objects.push_back(ROIs[i]); } diff --git a/modules/tracking/src/PFSolver.hpp b/modules/tracking/src/PFSolver.hpp index 520fa5f7e51..55c5b8193cb 100644 --- a/modules/tracking/src/PFSolver.hpp +++ b/modules/tracking/src/PFSolver.hpp @@ -1,5 +1,3 @@ -#include "opencv2/core.hpp" -#include "opencv2/core/core_c.h" #include #include #include diff --git a/modules/tracking/src/augmented_unscented_kalman.cpp b/modules/tracking/src/augmented_unscented_kalman.cpp index 85fdfabc42e..efc060b026b 100644 --- a/modules/tracking/src/augmented_unscented_kalman.cpp +++ b/modules/tracking/src/augmented_unscented_kalman.cpp @@ -42,10 +42,10 @@ #include "precomp.hpp" #include "opencv2/tracking/kalman_filters.hpp" -namespace cv -{ -namespace tracking -{ +namespace cv { +namespace detail { +inline namespace tracking { +inline namespace kalman_filters { void AugmentedUnscentedKalmanFilterParams:: init( int dp, int mp, int cp, double processNoiseCovDiag, double measurementNoiseCovDiag, @@ -394,5 +394,4 @@ Ptr createAugmentedUnscentedKalmanFilter(const AugmentedU return kfu; } -} // tracking -} // cv +}}}} // namespace diff --git a/modules/tracking/src/feature.cpp b/modules/tracking/src/feature.cpp index 7b8bfcff731..ecbceb57241 100644 --- a/modules/tracking/src/feature.cpp +++ b/modules/tracking/src/feature.cpp @@ -42,8 +42,9 @@ #include "precomp.hpp" #include "opencv2/tracking/feature.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { /* * TODO This implementation is based on apps/traincascade/ @@ -309,14 +310,14 @@ void CvHaarEvaluator::FeatureHaar::generateRandomFeature( Size patchSize ) position.y = rand() % ( patchSize.height ); position.x = rand() % ( patchSize.width ); - baseDim.width = (int) ( ( 1 - sqrt( 1 - (float) rand() / RAND_MAX ) ) * patchSize.width ); - baseDim.height = (int) ( ( 1 - sqrt( 1 - (float) rand() / RAND_MAX ) ) * patchSize.height ); + baseDim.width = (int) ( ( 1 - sqrt( 1 - (float) rand() * (float)(1.0 / RAND_MAX) ) ) * patchSize.width ); + baseDim.height = (int) ( ( 1 - sqrt( 1 - (float) rand() * (float)(1.0 / RAND_MAX) ) ) * patchSize.height ); //select types //float probType[11] = {0.0909f, 0.0909f, 0.0909f, 0.0909f, 0.0909f, 0.0909f, 0.0909f, 0.0909f, 0.0909f, 0.0909f, 0.0950f}; float probType[11] = { 0.2f, 0.2f, 0.2f, 0.2f, 0.2f, 0.2f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; - float prob = (float) rand() / RAND_MAX; + float prob = (float) rand() * (float)(1.0 / RAND_MAX); if( prob < probType[0] ) { @@ -686,7 +687,7 @@ void CvHaarEvaluator::FeatureHaar::generateRandomFeature( Size patchSize ) valid = true; } else - CV_Error(CV_StsAssert, ""); + CV_Error(Error::StsAssert, ""); } m_initSize = patchSize; @@ -1069,4 +1070,4 @@ void CvLBPEvaluator::Feature::write( FileStorage &fs ) const fs << CC_RECT << "[:" << rect.x << rect.y << rect.width << rect.height << "]"; } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/featureColorName.cpp b/modules/tracking/src/featureColorName.cpp index cb3aa3cf477..48bc360a50f 100644 --- a/modules/tracking/src/featureColorName.cpp +++ b/modules/tracking/src/featureColorName.cpp @@ -42,7 +42,10 @@ #include "precomp.hpp" #include -namespace cv{ +namespace cv { +namespace detail { +inline namespace tracking { + const float ColorNames[][10]={ {0.45975f,0.014802f,0.044289f,-0.028193f,0.001151f,-0.0050145f,0.34522f,0.018362f,0.23994f,0.1689f}, {0.47157f,0.021424f,0.041444f,-0.030215f,0.0019002f,-0.0029264f,0.32875f,0.0082059f,0.2502f,0.17007f}, @@ -32813,4 +32816,5 @@ namespace cv{ {0.0030858f,-0.016151f,0.013017f,0.0072284f,-0.53357f,0.30985f,0.0041336f,-0.012531f,0.00142f,-0.33842f}, {0.0087778f,-0.015645f,0.004769f,0.011785f,-0.54199f,0.31505f,0.00020476f,-0.020282f,0.00021236f,-0.34675f} }; -} + +}}} // namespace diff --git a/modules/tracking/src/gtrTracker.cpp b/modules/tracking/src/gtrTracker.cpp index c56bb5bbaff..b6dc1e004f7 100644 --- a/modules/tracking/src/gtrTracker.cpp +++ b/modules/tracking/src/gtrTracker.cpp @@ -38,96 +38,75 @@ // the use of this software, even if advised of the possibility of such damage. // //M*/ -#include "opencv2/opencv_modules.hpp" -#include "gtrTracker.hpp" +#include "precomp.hpp" +#ifdef HAVE_OPENCV_DNN +#include "opencv2/dnn.hpp" +#endif -namespace cv -{ +namespace cv { +inline namespace tracking { -TrackerGOTURN::Params::Params() +TrackerGOTURN::TrackerGOTURN() { - modelTxt = "goturn.prototxt"; - modelBin = "goturn.caffemodel"; + // nothing } -void TrackerGOTURN::Params::read(const cv::FileNode& /*fn*/){} - -void TrackerGOTURN::Params::write(cv::FileStorage& /*fs*/) const {} - - -Ptr TrackerGOTURN::create(const TrackerGOTURN::Params ¶meters) +TrackerGOTURN::~TrackerGOTURN() { -#ifdef HAVE_OPENCV_DNN - return Ptr(new gtr::TrackerGOTURNImpl(parameters)); -#else - (void)(parameters); - CV_Error(cv::Error::StsNotImplemented , "to use GOTURN, the tracking module needs to be built with opencv_dnn !"); -#endif + // nothing } -Ptr TrackerGOTURN::create() + +TrackerGOTURN::Params::Params() { - return TrackerGOTURN::create(TrackerGOTURN::Params()); + modelTxt = "goturn.prototxt"; + modelBin = "goturn.caffemodel"; } - #ifdef HAVE_OPENCV_DNN -namespace gtr -{ -class TrackerGOTURNModel : public TrackerModel{ +class TrackerGOTURNImpl : public TrackerGOTURN +{ public: - TrackerGOTURNModel(TrackerGOTURN::Params){} - Rect2d getBoundingBox(){ return boundingBox_; } - void setBoudingBox(Rect2d boundingBox) { + TrackerGOTURNImpl(const TrackerGOTURN::Params ¶meters) + : params(parameters) + { + // Load GOTURN architecture from *.prototxt and pretrained weights from *.caffemodel + net = dnn::readNetFromCaffe(params.modelTxt, params.modelBin); + CV_Assert(!net.empty()); + } + + void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + bool update(InputArray image, Rect& boundingBox) CV_OVERRIDE; + + void setBoudingBox(Rect boundingBox) + { if (image_.empty()) CV_Error(Error::StsInternal, "Set image first"); - boundingBox_ = boundingBox & Rect2d(Point(0, 0), image_.size()); + boundingBox_ = boundingBox & Rect(Point(0, 0), image_.size()); } - Mat getImage(){ return image_; } - void setImage(const Mat& image){ image.copyTo(image_); } -protected: - Rect2d boundingBox_; - Mat image_; - void modelEstimationImpl(const std::vector&) CV_OVERRIDE {} - void modelUpdateImpl() CV_OVERRIDE {} -}; - -TrackerGOTURNImpl::TrackerGOTURNImpl(const TrackerGOTURN::Params ¶meters) : - params(parameters){ - isInit = false; -}; -void TrackerGOTURNImpl::read(const cv::FileNode& fn) -{ - params.read(fn); -} + TrackerGOTURN::Params params; -void TrackerGOTURNImpl::write(cv::FileStorage& fs) const -{ - params.write(fs); -} + dnn::Net net; + Rect boundingBox_; + Mat image_; +}; -bool TrackerGOTURNImpl::initImpl(const Mat& image, const Rect2d& boundingBox) +void TrackerGOTURNImpl::init(InputArray image, const Rect& boundingBox) { - //Make a simple model from frame and bounding box - model = Ptr(new TrackerGOTURNModel(params)); - ((TrackerGOTURNModel*)static_cast(model))->setImage(image); - ((TrackerGOTURNModel*)static_cast(model))->setBoudingBox(boundingBox); - - //Load GOTURN architecture from *.prototxt and pretrained weights from *.caffemodel - net = dnn::readNetFromCaffe(params.modelTxt, params.modelBin); - return true; + image_ = image.getMat().clone(); + setBoudingBox(boundingBox); } -bool TrackerGOTURNImpl::updateImpl(const Mat& image, Rect2d& boundingBox) +bool TrackerGOTURNImpl::update(InputArray image, Rect& boundingBox) { int INPUT_SIZE = 227; //Using prevFrame & prevBB from model and curFrame GOTURN calculating curBB - Mat curFrame = image.clone(); - Mat prevFrame = ((TrackerGOTURNModel*)static_cast(model))->getImage(); - Rect2d prevBB = ((TrackerGOTURNModel*)static_cast(model))->getBoundingBox(); - Rect2d curBB; + InputArray curFrame = image; + Mat prevFrame = image_; + Rect2d prevBB = boundingBox_; + Rect curBB; float padTargetPatch = 2.0; Rect2f searchPatchRect, targetPatchRect; @@ -154,12 +133,12 @@ bool TrackerGOTURNImpl::updateImpl(const Mat& image, Rect2d& boundingBox) copyMakeBorder(curFrame, curFramePadded, (int)targetPatchRect.height, (int)targetPatchRect.height, (int)targetPatchRect.width, (int)targetPatchRect.width, BORDER_REPLICATE); searchPatch = curFramePadded(targetPatchRect).clone(); - //Preprocess - //Resize + // Preprocess + // Resize resize(targetPatch, targetPatch, Size(INPUT_SIZE, INPUT_SIZE), 0, 0, INTER_LINEAR_EXACT); resize(searchPatch, searchPatch, Size(INPUT_SIZE, INPUT_SIZE), 0, 0, INTER_LINEAR_EXACT); - //Convert to Float type and subtract mean + // Convert to Float type and subtract mean Mat targetBlob = dnn::blobFromImage(targetPatch, 1.0f, Size(), Scalar::all(128), false); Mat searchBlob = dnn::blobFromImage(searchPatch, 1.0f, Size(), Scalar::all(128), false); @@ -168,22 +147,31 @@ bool TrackerGOTURNImpl::updateImpl(const Mat& image, Rect2d& boundingBox) Mat resMat = net.forward("scale").reshape(1, 1); - curBB.x = targetPatchRect.x + (resMat.at(0) * targetPatchRect.width / INPUT_SIZE) - targetPatchRect.width; - curBB.y = targetPatchRect.y + (resMat.at(1) * targetPatchRect.height / INPUT_SIZE) - targetPatchRect.height; - curBB.width = (resMat.at(2) - resMat.at(0)) * targetPatchRect.width / INPUT_SIZE; - curBB.height = (resMat.at(3) - resMat.at(1)) * targetPatchRect.height / INPUT_SIZE; + curBB.x = cvRound(targetPatchRect.x + (resMat.at(0) * targetPatchRect.width / INPUT_SIZE) - targetPatchRect.width); + curBB.y = cvRound(targetPatchRect.y + (resMat.at(1) * targetPatchRect.height / INPUT_SIZE) - targetPatchRect.height); + curBB.width = cvRound((resMat.at(2) - resMat.at(0)) * targetPatchRect.width / INPUT_SIZE); + curBB.height = cvRound((resMat.at(3) - resMat.at(1)) * targetPatchRect.height / INPUT_SIZE); - //Predicted BB - boundingBox = curBB; - - //Set new model image and BB from current frame - ((TrackerGOTURNModel*)static_cast(model))->setImage(curFrame); - ((TrackerGOTURNModel*)static_cast(model))->setBoudingBox(curBB); + // Predicted BB + boundingBox = curBB & Rect(Point(0, 0), image_.size()); + // Set new model image and BB from current frame + image_ = image.getMat().clone(); + setBoudingBox(curBB); return true; } +Ptr TrackerGOTURN::create(const TrackerGOTURN::Params& parameters) +{ + return makePtr(parameters); +} + +#else // OPENCV_HAVE_DNN +Ptr TrackerGOTURN::create(const TrackerGOTURN::Params& parameters) +{ + (void)(parameters); + CV_Error(cv::Error::StsNotImplemented, "to use GOTURN, the tracking module needs to be built with opencv_dnn !"); } #endif // OPENCV_HAVE_DNN -} +}} // namespace diff --git a/modules/tracking/src/gtrTracker.hpp b/modules/tracking/src/gtrTracker.hpp deleted file mode 100644 index cd2820da5fd..00000000000 --- a/modules/tracking/src/gtrTracker.hpp +++ /dev/null @@ -1,80 +0,0 @@ - -/*M/////////////////////////////////////////////////////////////////////////////////////// -// -// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. -// -// By downloading, copying, installing or using the software you agree to this license. -// If you do not agree to this license, do not download, install, -// copy or use the software. -// -// -// License Agreement -// For Open Source Computer Vision Library -// -// Copyright (C) 2013, OpenCV Foundation, all rights reserved. -// Third party copyrights are property of their respective owners. -// -// Redistribution and use in source and binary forms, with or without modification, -// are permitted provided that the following conditions are met: -// -// * Redistribution's of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Redistribution's in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * The name of the copyright holders may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// This software is provided by the copyright holders and contributors "as is" and -// any express or implied warranties, including, but not limited to, the implied -// warranties of merchantability and fitness for a particular purpose are disclaimed. -// In no event shall the Intel Corporation or contributors be liable for any direct, -// indirect, incidental, special, exemplary, or consequential damages -// (including, but not limited to, procurement of substitute goods or services; -// loss of use, data, or profits; or business interruption) however caused -// and on any theory of liability, whether in contract, strict liability, -// or tort (including negligence or otherwise) arising in any way out of -// the use of this software, even if advised of the possibility of such damage. -// -//M*/ - -#ifndef OPENCV_GOTURN_TRACKER -#define OPENCV_GOTURN_TRACKER - -#include "precomp.hpp" -#include "opencv2/video/tracking.hpp" -#include "gtrUtils.hpp" -#include "opencv2/imgproc.hpp" - -#include -#include - -#include "opencv2/opencv_modules.hpp" -#ifdef HAVE_OPENCV_DNN -#include "opencv2/dnn.hpp" - -namespace cv -{ -namespace gtr -{ - -class TrackerGOTURNImpl : public TrackerGOTURN -{ -public: - TrackerGOTURNImpl(const TrackerGOTURN::Params ¶meters = TrackerGOTURN::Params()); - void read(const FileNode& fn) CV_OVERRIDE; - void write(FileStorage& fs) const CV_OVERRIDE; - bool initImpl(const Mat& image, const Rect2d& boundingBox) CV_OVERRIDE; - bool updateImpl(const Mat& image, Rect2d& boundingBox) CV_OVERRIDE; - - TrackerGOTURN::Params params; - - dnn::Net net; -}; - -} -} -#endif -#endif diff --git a/modules/tracking/src/gtrUtils.cpp b/modules/tracking/src/gtrUtils.cpp index e80dda1eadf..d2be7588d37 100644 --- a/modules/tracking/src/gtrUtils.cpp +++ b/modules/tracking/src/gtrUtils.cpp @@ -39,6 +39,7 @@ // //M*/ +#include "precomp.hpp" #include "gtrUtils.hpp" diff --git a/modules/tracking/src/gtrUtils.hpp b/modules/tracking/src/gtrUtils.hpp index fedf26f8864..0a3dea5adab 100644 --- a/modules/tracking/src/gtrUtils.hpp +++ b/modules/tracking/src/gtrUtils.hpp @@ -1,7 +1,6 @@ #ifndef OPENCV_GTR_UTILS #define OPENCV_GTR_UTILS -#include "precomp.hpp" #include namespace cv diff --git a/modules/tracking/src/kuhn_munkres.cpp b/modules/tracking/src/kuhn_munkres.cpp index 1d33c70e42c..5d0acee85d5 100644 --- a/modules/tracking/src/kuhn_munkres.cpp +++ b/modules/tracking/src/kuhn_munkres.cpp @@ -2,12 +2,17 @@ // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html. +#include "precomp.hpp" #include "kuhn_munkres.hpp" #include #include #include +namespace cv { +namespace detail { +inline namespace tracking { + KuhnMunkres::KuhnMunkres() : n_() {} std::vector KuhnMunkres::Solve(const cv::Mat& dissimilarity_matrix) { @@ -166,3 +171,6 @@ void KuhnMunkres::Run() { } } } + + +}}} // namespace \ No newline at end of file diff --git a/modules/tracking/src/kuhn_munkres.hpp b/modules/tracking/src/kuhn_munkres.hpp index 4f5ea28c185..d093d33af27 100644 --- a/modules/tracking/src/kuhn_munkres.hpp +++ b/modules/tracking/src/kuhn_munkres.hpp @@ -10,6 +10,9 @@ #include #include +namespace cv { +namespace detail { +inline namespace tracking { /// /// \brief The KuhnMunkres class @@ -52,4 +55,6 @@ class KuhnMunkres { int FindInCol(int col, int what); void Run(); }; + +}}} // namespace #endif // #ifndef __OPENCV_TRACKING_KUHN_MUNKRES_HPP__ diff --git a/modules/tracking/src/legacy/tracker.legacy.hpp b/modules/tracking/src/legacy/tracker.legacy.hpp index f692b5d62f9..788a758bcd2 100644 --- a/modules/tracking/src/legacy/tracker.legacy.hpp +++ b/modules/tracking/src/legacy/tracker.legacy.hpp @@ -39,14 +39,16 @@ // //M*/ -#include "precomp.hpp" +#include "opencv2/tracking/tracking_legacy.hpp" -namespace cv -{ +namespace cv { +namespace legacy { +inline namespace tracking { -/* - * Tracker - */ +Tracker::Tracker() +{ + isInit = false; +} Tracker::~Tracker() { @@ -97,4 +99,42 @@ bool Tracker::update( InputArray image, Rect2d& boundingBox ) return updateImpl( image.getMat(), boundingBox ); } -} /* namespace cv */ + + +class LegacyTrackerWrapper : public cv::tracking::Tracker +{ + const Ptr legacy_tracker_; +public: + LegacyTrackerWrapper(const Ptr& legacy_tracker) : legacy_tracker_(legacy_tracker) + { + CV_Assert(legacy_tracker_); + } + virtual ~LegacyTrackerWrapper() CV_OVERRIDE {}; + + void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE + { + CV_DbgAssert(legacy_tracker_); + legacy_tracker_->init(image, (Rect2d)boundingBox); + } + + bool update(InputArray image, CV_OUT Rect& boundingBox) CV_OVERRIDE + { + CV_DbgAssert(legacy_tracker_); + Rect2d boundingBox2d; + bool res = legacy_tracker_->update(image, boundingBox2d); + int x1 = cvRound(boundingBox2d.x); + int y1 = cvRound(boundingBox2d.y); + int x2 = cvRound(boundingBox2d.x + boundingBox2d.width); + int y2 = cvRound(boundingBox2d.y + boundingBox2d.height); + boundingBox = Rect(x1, y1, x2 - x1, y2 - y1) & Rect(Point(0, 0), image.size()); + return res; + } +}; + + +CV_EXPORTS_W Ptr upgradeTrackingAPI(const Ptr& legacy_tracker) +{ + return makePtr(legacy_tracker); +} + +}}} // namespace diff --git a/modules/tracking/src/legacy/trackerCSRT.legacy.hpp b/modules/tracking/src/legacy/trackerCSRT.legacy.hpp index 900fea51a64..b332ef25c08 100644 --- a/modules/tracking/src/legacy/trackerCSRT.legacy.hpp +++ b/modules/tracking/src/legacy/trackerCSRT.legacy.hpp @@ -2,657 +2,59 @@ // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html. -#include "precomp.hpp" +#include "opencv2/tracking/tracking_legacy.hpp" -#include "trackerCSRTSegmentation.hpp" -#include "trackerCSRTUtils.hpp" -#include "trackerCSRTScaleEstimation.hpp" +namespace cv { +namespace legacy { +inline namespace tracking { +namespace impl { -namespace cv -{ -/** -* \brief Implementation of TrackerModel for CSRT algorithm -*/ -class TrackerCSRTModel : public TrackerModel +class TrackerCSRTImpl CV_FINAL : public legacy::TrackerCSRT { public: - TrackerCSRTModel(TrackerCSRT::Params /*params*/){} - ~TrackerCSRTModel(){} -protected: - void modelEstimationImpl(const std::vector& /*responses*/) CV_OVERRIDE {} - void modelUpdateImpl() CV_OVERRIDE {} -}; - - -class TrackerCSRTImpl : public TrackerCSRT -{ -public: - TrackerCSRTImpl(const TrackerCSRT::Params ¶meters = TrackerCSRT::Params()); - void read(const FileNode& fn) CV_OVERRIDE; - void write(FileStorage& fs) const CV_OVERRIDE; - -protected: - TrackerCSRT::Params params; - - bool initImpl(const Mat& image, const Rect2d& boundingBox) CV_OVERRIDE; - virtual void setInitialMask(InputArray mask) CV_OVERRIDE; - bool updateImpl(const Mat& image, Rect2d& boundingBox) CV_OVERRIDE; - void update_csr_filter(const Mat &image, const Mat &my_mask); - void update_histograms(const Mat &image, const Rect ®ion); - void extract_histograms(const Mat &image, cv::Rect region, Histogram &hf, Histogram &hb); - std::vector create_csr_filter(const std::vector - img_features, const cv::Mat Y, const cv::Mat P); - Mat calculate_response(const Mat &image, const std::vector filter); - Mat get_location_prior(const Rect roi, const Size2f target_size, const Size img_sz); - Mat segment_region(const Mat &image, const Point2f &object_center, - const Size2f &template_size, const Size &target_size, float scale_factor); - Point2f estimate_new_position(const Mat &image); - std::vector get_features(const Mat &patch, const Size2i &feature_size); - -private: - bool check_mask_area(const Mat &mat, const double obj_area); - float current_scale_factor; - Mat window; - Mat yf; - Rect2f bounding_box; - std::vector csr_filter; - std::vector filter_weights; - Size2f original_target_size; - Size2i image_size; - Size2f template_size; - Size2i rescaled_template_size; - float rescale_ratio; - Point2f object_center; - DSST dsst; - Histogram hist_foreground; - Histogram hist_background; - double p_b; - Mat erode_element; - Mat filter_mask; - Mat preset_mask; - Mat default_mask; - float default_mask_area; - int cell_size; -}; - -Ptr TrackerCSRT::create(const TrackerCSRT::Params ¶meters) -{ - return Ptr(new TrackerCSRTImpl(parameters)); -} -Ptr TrackerCSRT::create() -{ - return Ptr(new TrackerCSRTImpl()); -} -TrackerCSRTImpl::TrackerCSRTImpl(const TrackerCSRT::Params ¶meters) : - params(parameters) -{ - isInit = false; -} - -void TrackerCSRTImpl::read(const cv::FileNode& fn) -{ - params.read(fn); -} - -void TrackerCSRTImpl::write(cv::FileStorage& fs) const -{ - params.write(fs); -} - -void TrackerCSRTImpl::setInitialMask(InputArray mask) -{ - preset_mask = mask.getMat(); -} - -bool TrackerCSRTImpl::check_mask_area(const Mat &mat, const double obj_area) -{ - double threshold = 0.05; - double mask_area= sum(mat)[0]; - if(mask_area < threshold*obj_area) { - return false; - } - return true; -} - -Mat TrackerCSRTImpl::calculate_response(const Mat &image, const std::vector filter) -{ - Mat patch = get_subwindow(image, object_center, cvFloor(current_scale_factor * template_size.width), - cvFloor(current_scale_factor * template_size.height)); - resize(patch, patch, rescaled_template_size, 0, 0, INTER_CUBIC); - - std::vector ftrs = get_features(patch, yf.size()); - std::vector Ffeatures = fourier_transform_features(ftrs); - Mat resp, res; - if(params.use_channel_weights){ - res = Mat::zeros(Ffeatures[0].size(), CV_32FC2); - Mat resp_ch; - Mat mul_mat; - for(size_t i = 0; i < Ffeatures.size(); ++i) { - mulSpectrums(Ffeatures[i], filter[i], resp_ch, 0, true); - res += (resp_ch * filter_weights[i]); - } - idft(res, res, DFT_SCALE | DFT_REAL_OUTPUT); - } else { - res = Mat::zeros(Ffeatures[0].size(), CV_32FC2); - Mat resp_ch; - for(size_t i = 0; i < Ffeatures.size(); ++i) { - mulSpectrums(Ffeatures[i], filter[i], resp_ch, 0 , true); - res = res + resp_ch; - } - idft(res, res, DFT_SCALE | DFT_REAL_OUTPUT); - } - return res; -} - -void TrackerCSRTImpl::update_csr_filter(const Mat &image, const Mat &mask) -{ - Mat patch = get_subwindow(image, object_center, cvFloor(current_scale_factor * template_size.width), - cvFloor(current_scale_factor * template_size.height)); - resize(patch, patch, rescaled_template_size, 0, 0, INTER_CUBIC); + cv::tracking::impl::TrackerCSRTImpl impl; - std::vector ftrs = get_features(patch, yf.size()); - std::vector Fftrs = fourier_transform_features(ftrs); - std::vector new_csr_filter = create_csr_filter(Fftrs, yf, mask); - //calculate per channel weights - if(params.use_channel_weights) { - Mat current_resp; - double max_val; - float sum_weights = 0; - std::vector new_filter_weights = std::vector(new_csr_filter.size()); - for(size_t i = 0; i < new_csr_filter.size(); ++i) { - mulSpectrums(Fftrs[i], new_csr_filter[i], current_resp, 0, true); - idft(current_resp, current_resp, DFT_SCALE | DFT_REAL_OUTPUT); - minMaxLoc(current_resp, NULL, &max_val, NULL, NULL); - sum_weights += static_cast(max_val); - new_filter_weights[i] = static_cast(max_val); - } - //update filter weights with new values - float updated_sum = 0; - for(size_t i = 0; i < filter_weights.size(); ++i) { - filter_weights[i] = filter_weights[i]*(1.0f - params.weights_lr) + - params.weights_lr * (new_filter_weights[i] / sum_weights); - updated_sum += filter_weights[i]; - } - //normalize weights - for(size_t i = 0; i < filter_weights.size(); ++i) { - filter_weights[i] /= updated_sum; - } - } - for(size_t i = 0; i < csr_filter.size(); ++i) { - csr_filter[i] = (1.0f - params.filter_lr)*csr_filter[i] + params.filter_lr * new_csr_filter[i]; + TrackerCSRTImpl(const legacy::TrackerCSRT::Params ¶meters) + : impl(parameters) + { + isInit = false; } - std::vector().swap(ftrs); - std::vector().swap(Fftrs); -} - -std::vector TrackerCSRTImpl::get_features(const Mat &patch, const Size2i &feature_size) -{ - std::vector features; - if (params.use_hog) { - std::vector hog = get_features_hog(patch, cell_size); - features.insert(features.end(), hog.begin(), - hog.begin()+params.num_hog_channels_used); - } - if (params.use_color_names) { - std::vector cn; - cn = get_features_cn(patch, feature_size); - features.insert(features.end(), cn.begin(), cn.end()); - } - if(params.use_gray) { - Mat gray_m; - cvtColor(patch, gray_m, COLOR_BGR2GRAY); - resize(gray_m, gray_m, feature_size, 0, 0, INTER_CUBIC); - gray_m.convertTo(gray_m, CV_32FC1, 1.0/255.0, -0.5); - features.push_back(gray_m); - } - if(params.use_rgb) { - std::vector rgb_features = get_features_rgb(patch, feature_size); - features.insert(features.end(), rgb_features.begin(), rgb_features.end()); + void read(const FileNode& fn) CV_OVERRIDE + { + static_cast(impl.params).read(fn); } - - for (size_t i = 0; i < features.size(); ++i) { - features.at(i) = features.at(i).mul(window); + void write(FileStorage& fs) const CV_OVERRIDE + { + static_cast(impl.params).write(fs); } - return features; -} -class ParallelCreateCSRFilter : public ParallelLoopBody { -public: - ParallelCreateCSRFilter( - const std::vector img_features, - const cv::Mat Y, - const cv::Mat P, - int admm_iterations, - std::vector &result_filter_): - result_filter(result_filter_) + bool initImpl(const Mat& image, const Rect2d& boundingBox) CV_OVERRIDE { - this->img_features = img_features; - this->Y = Y; - this->P = P; - this->admm_iterations = admm_iterations; - } - virtual void operator ()(const Range& range) const CV_OVERRIDE + impl.init(image, boundingBox); + model = impl.model; + sampler = makePtr(); + featureSet = makePtr(); + isInit = true; + return true; + } + bool updateImpl(const Mat& image, Rect2d& boundingBox) CV_OVERRIDE { - for (int i = range.start; i < range.end; i++) { - float mu = 5.0f; - float beta = 3.0f; - float mu_max = 20.0f; - float lambda = mu / 100.0f; - - Mat F = img_features[i]; - - Mat Sxy, Sxx; - mulSpectrums(F, Y, Sxy, 0, true); - mulSpectrums(F, F, Sxx, 0, true); - - Mat H; - H = divide_complex_matrices(Sxy, (Sxx + lambda)); - idft(H, H, DFT_SCALE|DFT_REAL_OUTPUT); - H = H.mul(P); - dft(H, H, DFT_COMPLEX_OUTPUT); - Mat L = Mat::zeros(H.size(), H.type()); //Lagrangian multiplier - Mat G; - for(int iteration = 0; iteration < admm_iterations; ++iteration) { - G = divide_complex_matrices((Sxy + (mu * H) - L) , (Sxx + mu)); - idft((mu * G) + L, H, DFT_SCALE | DFT_REAL_OUTPUT); - float lm = 1.0f / (lambda+mu); - H = H.mul(P*lm); - dft(H, H, DFT_COMPLEX_OUTPUT); - - //Update variables for next iteration - L = L + mu * (G - H); - mu = min(mu_max, beta*mu); - } - result_filter[i] = H; - } + Rect bb; + bool res = impl.update(image, bb); + boundingBox = bb; + return res; } - ParallelCreateCSRFilter& operator=(const ParallelCreateCSRFilter &) { - return *this; + virtual void setInitialMask(InputArray mask) CV_OVERRIDE + { + impl.setInitialMask(mask); } - -private: - int admm_iterations; - Mat Y; - Mat P; - std::vector img_features; - std::vector &result_filter; }; +} // namespace -std::vector TrackerCSRTImpl::create_csr_filter( - const std::vector img_features, - const cv::Mat Y, - const cv::Mat P) -{ - std::vector result_filter; - result_filter.resize(img_features.size()); - ParallelCreateCSRFilter parallelCreateCSRFilter(img_features, Y, P, - params.admm_iterations, result_filter); - parallel_for_(Range(0, static_cast(result_filter.size())), parallelCreateCSRFilter); - - return result_filter; -} - -Mat TrackerCSRTImpl::get_location_prior( - const Rect roi, - const Size2f target_size, - const Size img_sz) -{ - int x1 = cvRound(max(min(roi.x-1, img_sz.width-1) , 0)); - int y1 = cvRound(max(min(roi.y-1, img_sz.height-1) , 0)); - - int x2 = cvRound(min(max(roi.width-1, 0) , img_sz.width-1)); - int y2 = cvRound(min(max(roi.height-1, 0) , img_sz.height-1)); - - Size target_sz; - target_sz.width = target_sz.height = cvFloor(min(target_size.width, target_size.height)); - - double cx = x1 + (x2-x1)/2.; - double cy = y1 + (y2-y1)/2.; - double kernel_size_width = 1.0/(0.5*static_cast(target_sz.width)*1.4142+1); - double kernel_size_height = 1.0/(0.5*static_cast(target_sz.height)*1.4142+1); - - cv::Mat kernel_weight = Mat::zeros(1 + cvFloor(y2 - y1) , 1+cvFloor(-(x1-cx) + (x2-cx)), CV_64FC1); - for (int y = y1; y < y2+1; ++y){ - double * weightPtr = kernel_weight.ptr(y); - double tmp_y = std::pow((cy-y)*kernel_size_height, 2); - for (int x = x1; x < x2+1; ++x){ - weightPtr[x] = kernel_epan(std::pow((cx-x)*kernel_size_width,2) + tmp_y); - } - } - - double max_val; - cv::minMaxLoc(kernel_weight, NULL, &max_val, NULL, NULL); - Mat fg_prior = kernel_weight / max_val; - fg_prior.setTo(0.5, fg_prior < 0.5); - fg_prior.setTo(0.9, fg_prior > 0.9); - return fg_prior; -} - -Mat TrackerCSRTImpl::segment_region( - const Mat &image, - const Point2f &object_center, - const Size2f &template_size, - const Size &target_size, - float scale_factor) -{ - Rect valid_pixels; - Mat patch = get_subwindow(image, object_center, cvFloor(scale_factor * template_size.width), - cvFloor(scale_factor * template_size.height), &valid_pixels); - Size2f scaled_target = Size2f(target_size.width * scale_factor, - target_size.height * scale_factor); - Mat fg_prior = get_location_prior( - Rect(0,0, patch.size().width, patch.size().height), - scaled_target , patch.size()); - - std::vector img_channels; - split(patch, img_channels); - std::pair probs = Segment::computePosteriors2(img_channels, 0, 0, patch.cols, patch.rows, - p_b, fg_prior, 1.0-fg_prior, hist_foreground, hist_background); - - Mat mask = Mat::zeros(probs.first.size(), probs.first.type()); - probs.first(valid_pixels).copyTo(mask(valid_pixels)); - double max_resp = get_max(mask); - threshold(mask, mask, max_resp / 2.0, 1, THRESH_BINARY); - mask.convertTo(mask, CV_32FC1, 1.0); - return mask; -} - - -void TrackerCSRTImpl::extract_histograms(const Mat &image, cv::Rect region, Histogram &hf, Histogram &hb) -{ - // get coordinates of the region - int x1 = std::min(std::max(0, region.x), image.cols-1); - int y1 = std::min(std::max(0, region.y), image.rows-1); - int x2 = std::min(std::max(0, region.x + region.width), image.cols-1); - int y2 = std::min(std::max(0, region.y + region.height), image.rows-1); - - // calculate coordinates of the background region - int offsetX = (x2-x1+1) / params.background_ratio; - int offsetY = (y2-y1+1) / params.background_ratio; - int outer_y1 = std::max(0, (int)(y1-offsetY)); - int outer_y2 = std::min(image.rows, (int)(y2+offsetY+1)); - int outer_x1 = std::max(0, (int)(x1-offsetX)); - int outer_x2 = std::min(image.cols, (int)(x2+offsetX+1)); - - // calculate probability for the background - p_b = 1.0 - ((x2-x1+1) * (y2-y1+1)) / - ((double) (outer_x2-outer_x1+1) * (outer_y2-outer_y1+1)); - - // split multi-channel image into the std::vector of matrices - std::vector img_channels(image.channels()); - split(image, img_channels); - for(size_t k=0; k().swap(img_channels); -} - -void TrackerCSRTImpl::update_histograms(const Mat &image, const Rect ®ion) -{ - // create temporary histograms - Histogram hf(image.channels(), params.histogram_bins); - Histogram hb(image.channels(), params.histogram_bins); - extract_histograms(image, region, hf, hb); - - // get histogram vectors from temporary histograms - std::vector hf_vect_new = hf.getHistogramVector(); - std::vector hb_vect_new = hb.getHistogramVector(); - // get histogram vectors from learned histograms - std::vector hf_vect = hist_foreground.getHistogramVector(); - std::vector hb_vect = hist_background.getHistogramVector(); - - // update histograms - use learning rate - for(size_t i=0; i().swap(hf_vect); - std::vector().swap(hb_vect); -} - -Point2f TrackerCSRTImpl::estimate_new_position(const Mat &image) -{ - - Mat resp = calculate_response(image, csr_filter); - - double max_val; - Point max_loc; - minMaxLoc(resp, NULL, &max_val, NULL, &max_loc); - if (max_val < params.psr_threshold) - return Point2f(-1,-1); // target "lost" - - // take into account also subpixel accuracy - float col = ((float) max_loc.x) + subpixel_peak(resp, "horizontal", max_loc); - float row = ((float) max_loc.y) + subpixel_peak(resp, "vertical", max_loc); - if(row + 1 > (float)resp.rows / 2.0f) { - row = row - resp.rows; - } - if(col + 1 > (float)resp.cols / 2.0f) { - col = col - resp.cols; - } - // calculate x and y displacements - Point2f new_center = object_center + Point2f(current_scale_factor * (1.0f / rescale_ratio) *cell_size*(col), - current_scale_factor * (1.0f / rescale_ratio) *cell_size*(row)); - //sanity checks - if(new_center.x < 0) - new_center.x = 0; - if(new_center.x >= image_size.width) - new_center.x = static_cast(image_size.width - 1); - if(new_center.y < 0) - new_center.y = 0; - if(new_center.y >= image_size.height) - new_center.y = static_cast(image_size.height - 1); - - return new_center; -} - -// ********************************************************************* -// * Update API function * -// ********************************************************************* -bool TrackerCSRTImpl::updateImpl(const Mat& image_, Rect2d& boundingBox) -{ - Mat image; - if(image_.channels() == 1) //treat gray image as color image - cvtColor(image_, image, COLOR_GRAY2BGR); - else - image = image_; - - object_center = estimate_new_position(image); - if (object_center.x < 0 && object_center.y < 0) - return false; - - current_scale_factor = dsst.getScale(image, object_center); - //update bouding_box according to new scale and location - bounding_box.x = object_center.x - current_scale_factor * original_target_size.width / 2.0f; - bounding_box.y = object_center.y - current_scale_factor * original_target_size.height / 2.0f; - bounding_box.width = current_scale_factor * original_target_size.width; - bounding_box.height = current_scale_factor * original_target_size.height; - - //update tracker - if(params.use_segmentation) { - Mat hsv_img = bgr2hsv(image); - update_histograms(hsv_img, bounding_box); - filter_mask = segment_region(hsv_img, object_center, - template_size,original_target_size, current_scale_factor); - resize(filter_mask, filter_mask, yf.size(), 0, 0, INTER_NEAREST); - if(check_mask_area(filter_mask, default_mask_area)) { - dilate(filter_mask , filter_mask, erode_element); - } else { - filter_mask = default_mask; - } - } else { - filter_mask = default_mask; - } - update_csr_filter(image, filter_mask); - dsst.update(image, object_center); - boundingBox = bounding_box; - return true; -} - -// ********************************************************************* -// * Init API function * -// ********************************************************************* -bool TrackerCSRTImpl::initImpl(const Mat& image_, const Rect2d& boundingBox) -{ - Mat image; - if(image_.channels() == 1) //treat gray image as color image - cvtColor(image_, image, COLOR_GRAY2BGR); - else - image = image_; - - current_scale_factor = 1.0; - image_size = image.size(); - bounding_box = boundingBox; - cell_size = cvFloor(std::min(4.0, std::max(1.0, static_cast( - cvCeil((bounding_box.width * bounding_box.height)/400.0))))); - original_target_size = Size(bounding_box.size()); - - template_size.width = static_cast(cvFloor(original_target_size.width + params.padding * - sqrt(original_target_size.width * original_target_size.height))); - template_size.height = static_cast(cvFloor(original_target_size.height + params.padding * - sqrt(original_target_size.width * original_target_size.height))); - template_size.width = template_size.height = - (template_size.width + template_size.height) / 2.0f; - rescale_ratio = sqrt(pow(params.template_size,2) / (template_size.width * template_size.height)); - if(rescale_ratio > 1) { - rescale_ratio = 1; - } - rescaled_template_size = Size2i(cvFloor(template_size.width * rescale_ratio), - cvFloor(template_size.height * rescale_ratio)); - object_center = Point2f(static_cast(boundingBox.x) + original_target_size.width / 2.0f, - static_cast(boundingBox.y) + original_target_size.height / 2.0f); - - yf = gaussian_shaped_labels(params.gsl_sigma, - rescaled_template_size.width / cell_size, rescaled_template_size.height / cell_size); - if(params.window_function.compare("hann") == 0) { - window = get_hann_win(Size(yf.cols,yf.rows)); - } else if(params.window_function.compare("cheb") == 0) { - window = get_chebyshev_win(Size(yf.cols,yf.rows), params.cheb_attenuation); - } else if(params.window_function.compare("kaiser") == 0) { - window = get_kaiser_win(Size(yf.cols,yf.rows), params.kaiser_alpha); - } else { - std::cout << "Not a valid window function" << std::endl; - return false; - } - - Size2i scaled_obj_size = Size2i(cvFloor(original_target_size.width * rescale_ratio / cell_size), - cvFloor(original_target_size.height * rescale_ratio / cell_size)); - //set dummy mask and area; - int x0 = std::max((yf.size().width - scaled_obj_size.width)/2 - 1, 0); - int y0 = std::max((yf.size().height - scaled_obj_size.height)/2 - 1, 0); - default_mask = Mat::zeros(yf.size(), CV_32FC1); - default_mask(Rect(x0,y0,scaled_obj_size.width, scaled_obj_size.height)) = 1.0f; - default_mask_area = static_cast(sum(default_mask)[0]); - - //initalize segmentation - if(params.use_segmentation) { - Mat hsv_img = bgr2hsv(image); - hist_foreground = Histogram(hsv_img.channels(), params.histogram_bins); - hist_background = Histogram(hsv_img.channels(), params.histogram_bins); - extract_histograms(hsv_img, bounding_box, hist_foreground, hist_background); - filter_mask = segment_region(hsv_img, object_center, template_size, - original_target_size, current_scale_factor); - //update calculated mask with preset mask - if(preset_mask.data){ - Mat preset_mask_padded = Mat::zeros(filter_mask.size(), filter_mask.type()); - int sx = std::max((int)cvFloor(preset_mask_padded.cols / 2.0f - preset_mask.cols / 2.0f) - 1, 0); - int sy = std::max((int)cvFloor(preset_mask_padded.rows / 2.0f - preset_mask.rows / 2.0f) - 1, 0); - preset_mask.copyTo(preset_mask_padded( - Rect(sx, sy, preset_mask.cols, preset_mask.rows))); - filter_mask = filter_mask.mul(preset_mask_padded); - } - erode_element = getStructuringElement(MORPH_ELLIPSE, Size(3,3), Point(1,1)); - resize(filter_mask, filter_mask, yf.size(), 0, 0, INTER_NEAREST); - if(check_mask_area(filter_mask, default_mask_area)) { - dilate(filter_mask , filter_mask, erode_element); - } else { - filter_mask = default_mask; - } - - } else { - filter_mask = default_mask; - } - - //initialize filter - Mat patch = get_subwindow(image, object_center, cvFloor(current_scale_factor * template_size.width), - cvFloor(current_scale_factor * template_size.height)); - resize(patch, patch, rescaled_template_size, 0, 0, INTER_CUBIC); - std::vector patch_ftrs = get_features(patch, yf.size()); - std::vector Fftrs = fourier_transform_features(patch_ftrs); - csr_filter = create_csr_filter(Fftrs, yf, filter_mask); - - if(params.use_channel_weights) { - Mat current_resp; - filter_weights = std::vector(csr_filter.size()); - float chw_sum = 0; - for (size_t i = 0; i < csr_filter.size(); ++i) { - mulSpectrums(Fftrs[i], csr_filter[i], current_resp, 0, true); - idft(current_resp, current_resp, DFT_SCALE | DFT_REAL_OUTPUT); - double max_val; - minMaxLoc(current_resp, NULL, &max_val, NULL , NULL); - chw_sum += static_cast(max_val); - filter_weights[i] = static_cast(max_val); - } - for (size_t i = 0; i < filter_weights.size(); ++i) { - filter_weights[i] /= chw_sum; - } - } - - //initialize scale search - dsst = DSST(image, bounding_box, template_size, params.number_of_scales, params.scale_step, - params.scale_model_max_area, params.scale_sigma_factor, params.scale_lr); - - model=Ptr(new TrackerCSRTModel(params)); - isInit = true; - return true; -} - -TrackerCSRT::Params::Params() -{ - use_channel_weights = true; - use_segmentation = true; - use_hog = true; - use_color_names = true; - use_gray = true; - use_rgb = false; - window_function = "hann"; - kaiser_alpha = 3.75f; - cheb_attenuation = 45; - padding = 3.0f; - template_size = 200; - gsl_sigma = 1.0f; - hog_orientations = 9; - hog_clip = 0.2f; - num_hog_channels_used = 18; - filter_lr = 0.02f; - weights_lr = 0.02f; - admm_iterations = 4; - number_of_scales = 33; - scale_sigma_factor = 0.250f; - scale_model_max_area = 512.0f; - scale_lr = 0.025f; - scale_step = 1.020f; - histogram_bins = 16; - background_ratio = 2; - histogram_lr = 0.04f; - psr_threshold = 0.035f; -} - -void TrackerCSRT::Params::read(const FileNode& fn) +void legacy::TrackerCSRT::Params::read(const FileNode& fn) { *this = TrackerCSRT::Params(); if(!fn["padding"].empty()) @@ -712,7 +114,7 @@ void TrackerCSRT::Params::read(const FileNode& fn) CV_Assert(number_of_scales % 2 == 1); CV_Assert(use_gray || use_color_names || use_hog || use_rgb); } -void TrackerCSRT::Params::write(FileStorage& fs) const +void legacy::TrackerCSRT::Params::write(FileStorage& fs) const { fs << "padding" << padding; fs << "template_size" << template_size; @@ -742,4 +144,16 @@ void TrackerCSRT::Params::write(FileStorage& fs) const fs << "histogram_lr" << histogram_lr; fs << "psr_threshold" << psr_threshold; } -} /* namespace cv */ + +}} // namespace + +Ptr legacy::TrackerCSRT::create(const legacy::TrackerCSRT::Params ¶meters) +{ + return makePtr(parameters); +} +Ptr legacy::TrackerCSRT::create() +{ + return create(legacy::TrackerCSRT::Params()); +} + +} // namespace diff --git a/modules/tracking/src/legacy/trackerKCF.legacy.hpp b/modules/tracking/src/legacy/trackerKCF.legacy.hpp index 471ad71f7aa..86e895ec284 100644 --- a/modules/tracking/src/legacy/trackerKCF.legacy.hpp +++ b/modules/tracking/src/legacy/trackerKCF.legacy.hpp @@ -39,893 +39,61 @@ // //M*/ -#include "precomp.hpp" -#include "opencl_kernels_tracking.hpp" -#include -#include - -/*--------------------------- -| TrackerKCFModel -|---------------------------*/ -namespace cv{ - /** - * \brief Implementation of TrackerModel for KCF algorithm - */ - class TrackerKCFModel : public TrackerModel{ - public: - TrackerKCFModel(TrackerKCF::Params /*params*/){} - ~TrackerKCFModel(){} - protected: - void modelEstimationImpl( const std::vector& /*responses*/ ) CV_OVERRIDE {} - void modelUpdateImpl() CV_OVERRIDE {} - }; -} /* namespace cv */ +#include "opencv2/tracking/tracking_legacy.hpp" +namespace cv { +namespace legacy { +inline namespace tracking { +namespace impl { /*--------------------------- | TrackerKCF |---------------------------*/ -namespace cv{ - - /* - * Prototype - */ - class TrackerKCFImpl : public TrackerKCF { - public: - TrackerKCFImpl( const TrackerKCF::Params ¶meters = TrackerKCF::Params() ); - void read( const FileNode& /*fn*/ ) CV_OVERRIDE; - void write( FileStorage& /*fs*/ ) const CV_OVERRIDE; - void setFeatureExtractor(void (*f)(const Mat, const Rect, Mat&), bool pca_func = false) CV_OVERRIDE; - - protected: - /* - * basic functions and vars - */ - bool initImpl( const Mat& /*image*/, const Rect2d& boundingBox ) CV_OVERRIDE; - bool updateImpl( const Mat& image, Rect2d& boundingBox ) CV_OVERRIDE; - - TrackerKCF::Params params; - - /* - * KCF functions and vars - */ - void createHanningWindow(OutputArray dest, const cv::Size winSize, const int type) const; - void inline fft2(const Mat src, std::vector & dest, std::vector & layers_data) const; - void inline fft2(const Mat src, Mat & dest) const; - void inline ifft2(const Mat src, Mat & dest) const; - void inline pixelWiseMult(const std::vector src1, const std::vector src2, std::vector & dest, const int flags, const bool conjB=false) const; - void inline sumChannels(std::vector src, Mat & dest) const; - void inline updateProjectionMatrix(const Mat src, Mat & old_cov,Mat & proj_matrix,float pca_rate, int compressed_sz, - std::vector & layers_pca,std::vector & average, Mat pca_data, Mat new_cov, Mat w, Mat u, Mat v); - void inline compress(const Mat proj_matrix, const Mat src, Mat & dest, Mat & data, Mat & compressed) const; - bool getSubWindow(const Mat img, const Rect roi, Mat& feat, Mat& patch, TrackerKCF::MODE desc = GRAY) const; - bool getSubWindow(const Mat img, const Rect roi, Mat& feat, void (*f)(const Mat, const Rect, Mat& )) const; - void extractCN(Mat patch_data, Mat & cnFeatures) const; - void denseGaussKernel(const float sigma, const Mat , const Mat y_data, Mat & k_data, - std::vector & layers_data,std::vector & xf_data,std::vector & yf_data, std::vector xyf_v, Mat xy, Mat xyf ) const; - void calcResponse(const Mat alphaf_data, const Mat kf_data, Mat & response_data, Mat & spec_data) const; - void calcResponse(const Mat alphaf_data, const Mat alphaf_den_data, const Mat kf_data, Mat & response_data, Mat & spec_data, Mat & spec2_data) const; - - void shiftRows(Mat& mat) const; - void shiftRows(Mat& mat, int n) const; - void shiftCols(Mat& mat, int n) const; -#ifdef HAVE_OPENCL - bool inline oclTransposeMM(const Mat src, float alpha, UMat &dst); -#endif - - private: - float output_sigma; - Rect2d roi; - Mat hann; //hann window filter - Mat hann_cn; //10 dimensional hann-window filter for CN features, - - Mat y,yf; // training response and its FFT - Mat x; // observation and its FFT - Mat k,kf; // dense gaussian kernel and its FFT - Mat kf_lambda; // kf+lambda - Mat new_alphaf, alphaf; // training coefficients - Mat new_alphaf_den, alphaf_den; // for splitted training coefficients - Mat z; // model - Mat response; // detection result - Mat old_cov_mtx, proj_mtx; // for feature compression - - // pre-defined Mat variables for optimization of private functions - Mat spec, spec2; - std::vector layers; - std::vector vxf,vyf,vxyf; - Mat xy_data,xyf_data; - Mat data_temp, compress_data; - std::vector layers_pca_data; - std::vector average_data; - Mat img_Patch; +class TrackerKCFImpl CV_FINAL : public legacy::TrackerKCF +{ +public: + cv::tracking::impl::TrackerKCFImpl impl; - // storage for the extracted features, KRLS model, KRLS compressed model - Mat X[2],Z[2],Zc[2]; - - // storage of the extracted features - std::vector features_pca; - std::vector features_npca; - std::vector descriptors_pca; - std::vector descriptors_npca; - - // optimization variables for updateProjectionMatrix - Mat data_pca, new_covar,w_data,u_data,vt_data; - - // custom feature extractor - bool use_custom_extractor_pca; - bool use_custom_extractor_npca; - std::vector extractor_pca; - std::vector extractor_npca; - - bool resizeImage; // resize the image whenever needed and the patch size is large - -#ifdef HAVE_OPENCL - ocl::Kernel transpose_mm_ker; // OCL kernel to compute transpose matrix multiply matrix. -#endif - - int frame; - }; - - /* - * Constructor - */ - Ptr TrackerKCF::create(const TrackerKCF::Params ¶meters){ - return Ptr(new TrackerKCFImpl(parameters)); - } - Ptr TrackerKCF::create(){ - return Ptr(new TrackerKCFImpl()); - } - TrackerKCFImpl::TrackerKCFImpl( const TrackerKCF::Params ¶meters ) : - params( parameters ) - { - isInit = false; - resizeImage = false; - use_custom_extractor_pca = false; - use_custom_extractor_npca = false; - -#ifdef HAVE_OPENCL - // For update proj matrix's multiplication - if(ocl::useOpenCL()) + TrackerKCFImpl(const legacy::TrackerKCF::Params ¶meters) + : impl(parameters) { - cv::String err; - ocl::ProgramSource tmmSrc = ocl::tracking::tmm_oclsrc; - ocl::Program tmmProg(tmmSrc, String(), err); - transpose_mm_ker.create("tmm", tmmProg); - } -#endif - } - - void TrackerKCFImpl::read( const cv::FileNode& fn ){ - params.read( fn ); - } - - void TrackerKCFImpl::write( cv::FileStorage& fs ) const { - params.write( fs ); - } - - /* - * Initialization: - * - creating hann window filter - * - ROI padding - * - creating a gaussian response for the training ground-truth - * - perform FFT to the gaussian response - */ - bool TrackerKCFImpl::initImpl( const Mat& image, const Rect2d& boundingBox ){ - frame=0; - roi.x = cvRound(boundingBox.x); - roi.y = cvRound(boundingBox.y); - roi.width = cvRound(boundingBox.width); - roi.height = cvRound(boundingBox.height); - - //calclulate output sigma - output_sigma=std::sqrt(static_cast(roi.width*roi.height))*params.output_sigma_factor; - output_sigma=-0.5f/(output_sigma*output_sigma); - - //resize the ROI whenever needed - if(params.resize && roi.width*roi.height>params.max_patch_size){ - resizeImage=true; - roi.x/=2.0; - roi.y/=2.0; - roi.width/=2.0; - roi.height/=2.0; - } - - // add padding to the roi - roi.x-=roi.width/2; - roi.y-=roi.height/2; - roi.width*=2; - roi.height*=2; - - // initialize the hann window filter - createHanningWindow(hann, roi.size(), CV_32F); - - // hann window filter for CN feature - Mat _layer[] = {hann, hann, hann, hann, hann, hann, hann, hann, hann, hann}; - merge(_layer, 10, hann_cn); - - // create gaussian response - y=Mat::zeros((int)roi.height,(int)roi.width,CV_32F); - for(int i=0;i(i,j) = - static_cast((i-roi.height/2+1)*(i-roi.height/2+1)+(j-roi.width/2+1)*(j-roi.width/2+1)); - } - } - - y*=(float)output_sigma; - cv::exp(y,y); - - // perform fourier transfor to the gaussian response - fft2(y,yf); - - if (image.channels() == 1) { // disable CN for grayscale images - params.desc_pca &= ~(CN); - params.desc_npca &= ~(CN); - } - model=Ptr(new TrackerKCFModel(params)); - - // record the non-compressed descriptors - if((params.desc_npca & GRAY) == GRAY)descriptors_npca.push_back(GRAY); - if((params.desc_npca & CN) == CN)descriptors_npca.push_back(CN); - if(use_custom_extractor_npca)descriptors_npca.push_back(CUSTOM); - features_npca.resize(descriptors_npca.size()); - - // record the compressed descriptors - if((params.desc_pca & GRAY) == GRAY)descriptors_pca.push_back(GRAY); - if((params.desc_pca & CN) == CN)descriptors_pca.push_back(CN); - if(use_custom_extractor_pca)descriptors_pca.push_back(CUSTOM); - features_pca.resize(descriptors_pca.size()); - - // accept only the available descriptor modes - CV_Assert( - (params.desc_pca & GRAY) == GRAY - || (params.desc_npca & GRAY) == GRAY - || (params.desc_pca & CN) == CN - || (params.desc_npca & CN) == CN - || use_custom_extractor_pca - || use_custom_extractor_npca - ); - - //return true only if roi has intersection with the image - if((roi & Rect2d(0,0, resizeImage ? image.cols / 2 : image.cols, - resizeImage ? image.rows / 2 : image.rows)) == Rect2d()) - return false; - - return true; - } - - /* - * Main part of the KCF algorithm - */ - bool TrackerKCFImpl::updateImpl( const Mat& image, Rect2d& boundingBox ){ - double minVal, maxVal; // min-max response - Point minLoc,maxLoc; // min-max location - - Mat img=image.clone(); - // check the channels of the input image, grayscale is preferred - CV_Assert(img.channels() == 1 || img.channels() == 3); - - // resize the image whenever needed - if(resizeImage)resize(img,img,Size(img.cols/2,img.rows/2),0,0,INTER_LINEAR_EXACT); - - // detection part - if(frame>0){ - - // extract and pre-process the patch - // get non compressed descriptors - for(unsigned i=0;i0)merge(features_npca,X[1]); - - // get compressed descriptors - for(unsigned i=0;i0)merge(features_pca,X[0]); - - //compress the features and the KRSL model - if(params.desc_pca !=0){ - compress(proj_mtx,X[0],X[0],data_temp,compress_data); - compress(proj_mtx,Z[0],Zc[0],data_temp,compress_data); - } - - // copy the compressed KRLS model - Zc[1] = Z[1]; - - // merge all features - if(features_npca.size()==0){ - x = X[0]; - z = Zc[0]; - }else if(features_pca.size()==0){ - x = X[1]; - z = Z[1]; - }else{ - merge(X,2,x); - merge(Zc,2,z); - } - - //compute the gaussian kernel - denseGaussKernel(params.sigma,x,z,k,layers,vxf,vyf,vxyf,xy_data,xyf_data); - - // compute the fourier transform of the kernel - fft2(k,kf); - if(frame==1)spec2=Mat_(kf.rows, kf.cols); - - // calculate filter response - if(params.split_coeff) - calcResponse(alphaf,alphaf_den,kf,response, spec, spec2); - else - calcResponse(alphaf,kf,response, spec); - - // extract the maximum response - minMaxLoc( response, &minVal, &maxVal, &minLoc, &maxLoc ); - if (maxVal < params.detect_thresh) - { - return false; - } - roi.x+=(maxLoc.x-roi.width/2+1); - roi.y+=(maxLoc.y-roi.height/2+1); - } - - // update the bounding box - boundingBox.x=(resizeImage?roi.x*2:roi.x)+(resizeImage?roi.width*2:roi.width)/4; - boundingBox.y=(resizeImage?roi.y*2:roi.y)+(resizeImage?roi.height*2:roi.height)/4; - boundingBox.width = (resizeImage?roi.width*2:roi.width)/2; - boundingBox.height = (resizeImage?roi.height*2:roi.height)/2; - - // extract the patch for learning purpose - // get non compressed descriptors - for(unsigned i=0;i0)merge(features_npca,X[1]); - - // get compressed descriptors - for(unsigned i=0;i0)merge(features_pca,X[0]); - - //update the training data - if(frame==0){ - Z[0] = X[0].clone(); - Z[1] = X[1].clone(); - }else{ - Z[0]=(1.0-params.interp_factor)*Z[0]+params.interp_factor*X[0]; - Z[1]=(1.0-params.interp_factor)*Z[1]+params.interp_factor*X[1]; - } - - if(params.desc_pca !=0 || use_custom_extractor_pca){ - // initialize the vector of Mat variables - if(frame==0){ - layers_pca_data.resize(Z[0].channels()); - average_data.resize(Z[0].channels()); - } - - // feature compression - updateProjectionMatrix(Z[0],old_cov_mtx,proj_mtx,params.pca_learning_rate,params.compressed_size,layers_pca_data,average_data,data_pca, new_covar,w_data,u_data,vt_data); - compress(proj_mtx,X[0],X[0],data_temp,compress_data); - } - - // merge all features - if(features_npca.size()==0) - x = X[0]; - else if(features_pca.size()==0) - x = X[1]; - else - merge(X,2,x); - - // initialize some required Mat variables - if(frame==0){ - layers.resize(x.channels()); - vxf.resize(x.channels()); - vyf.resize(x.channels()); - vxyf.resize(vyf.size()); - new_alphaf=Mat_(yf.rows, yf.cols); - } - - // Kernel Regularized Least-Squares, calculate alphas - denseGaussKernel(params.sigma,x,x,k,layers,vxf,vyf,vxyf,xy_data,xyf_data); - - // compute the fourier transform of the kernel and add a small value - fft2(k,kf); - kf_lambda=kf+params.lambda; - - float den; - if(params.split_coeff){ - mulSpectrums(yf,kf,new_alphaf,0); - mulSpectrums(kf,kf_lambda,new_alphaf_den,0); - }else{ - for(int i=0;i(i,j)[0]*kf_lambda.at(i,j)[0]+kf_lambda.at(i,j)[1]*kf_lambda.at(i,j)[1]); - - new_alphaf.at(i,j)[0]= - (yf.at(i,j)[0]*kf_lambda.at(i,j)[0]+yf.at(i,j)[1]*kf_lambda.at(i,j)[1])*den; - new_alphaf.at(i,j)[1]= - (yf.at(i,j)[1]*kf_lambda.at(i,j)[0]-yf.at(i,j)[0]*kf_lambda.at(i,j)[1])*den; - } - } - } - - // update the RLS model - if(frame==0){ - alphaf=new_alphaf.clone(); - if(params.split_coeff)alphaf_den=new_alphaf_den.clone(); - }else{ - alphaf=(1.0-params.interp_factor)*alphaf+params.interp_factor*new_alphaf; - if(params.split_coeff)alphaf_den=(1.0-params.interp_factor)*alphaf_den+params.interp_factor*new_alphaf_den; - } - - frame++; - return true; - } - - - /*------------------------------------- - | implementation of the KCF functions - |-------------------------------------*/ - - /* - * hann window filter - */ - void TrackerKCFImpl::createHanningWindow(OutputArray dest, const cv::Size winSize, const int type) const { - CV_Assert( type == CV_32FC1 || type == CV_64FC1 ); - - dest.create(winSize, type); - Mat dst = dest.getMat(); - - int rows = dst.rows, cols = dst.cols; - - AutoBuffer _wc(cols); - float * const wc = _wc.data(); - - const float coeff0 = 2.0f * (float)CV_PI / (cols - 1); - const float coeff1 = 2.0f * (float)CV_PI / (rows - 1); - for(int j = 0; j < cols; j++) - wc[j] = 0.5f * (1.0f - cos(coeff0 * j)); - - if(dst.depth() == CV_32F){ - for(int i = 0; i < rows; i++){ - float* dstData = dst.ptr(i); - float wr = 0.5f * (1.0f - cos(coeff1 * i)); - for(int j = 0; j < cols; j++) - dstData[j] = (float)(wr * wc[j]); - } - }else{ - for(int i = 0; i < rows; i++){ - double* dstData = dst.ptr(i); - double wr = 0.5f * (1.0f - cos(coeff1 * i)); - for(int j = 0; j < cols; j++) - dstData[j] = wr * wc[j]; - } - } - - // perform batch sqrt for SSE performance gains - //cv::sqrt(dst, dst); //matlab do not use the square rooted version - } - - /* - * simplification of fourier transform function in opencv - */ - void inline TrackerKCFImpl::fft2(const Mat src, Mat & dest) const { - dft(src,dest,DFT_COMPLEX_OUTPUT); - } - - void inline TrackerKCFImpl::fft2(const Mat src, std::vector & dest, std::vector & layers_data) const { - split(src, layers_data); - - for(int i=0;i src1, const std::vector src2, std::vector & dest, const int flags, const bool conjB) const { - for(unsigned i=0;i src, Mat & dest) const { - dest=src[0].clone(); - for(unsigned i=1;i(src.cols * 64), static_cast(src.cols)}; - size_t localSize[2] = {64, 1}; - if (!transpose_mm_ker.run(2, globSize, localSize, true)) - return false; - return true; - } -#endif - - /* - * obtains the projection matrix using PCA - */ - void inline TrackerKCFImpl::updateProjectionMatrix(const Mat src, Mat & old_cov,Mat & proj_matrix, float pca_rate, int compressed_sz, - std::vector & layers_pca,std::vector & average, Mat pca_data, Mat new_cov, Mat w, Mat u, Mat vt) { - CV_Assert(compressed_sz<=src.channels()); - - split(src,layers_pca); - - for (int i=0;i(i, j) - result.getMat(ACCESS_RW).at(i , j)) > abs(new_cov.at(i, j)) * 1e-3) - printf("error @ i %d j %d got %G expected %G \n", i, j, result.getMat(ACCESS_RW).at(i , j), new_cov.at(i, j)); -#endif - if(old_cov.rows==0)old_cov=new_cov.clone(); - SVD::compute((1.0f - pca_rate) * old_cov + pca_rate * new_cov, w, u, vt); - } -#else - new_cov=1.0/(float)(src.rows*src.cols-1)*(pca_data.t()*pca_data); - if(old_cov.rows==0)old_cov=new_cov.clone(); - - // calc PCA - SVD::compute((1.0-pca_rate)*old_cov+pca_rate*new_cov, w, u, vt); -#endif - // extract the projection matrix - proj_matrix=u(Rect(0,0,compressed_sz,src.channels())).clone(); - Mat proj_vars=Mat::eye(compressed_sz,compressed_sz,proj_matrix.type()); - for(int i=0;i(i,i)=w.at(i); + isInit = false; } - - // update the covariance matrix - old_cov=(1.0-pca_rate)*old_cov+pca_rate*proj_matrix*proj_vars*proj_matrix.t(); - } - - /* - * compress the features - */ - void inline TrackerKCFImpl::compress(const Mat proj_matrix, const Mat src, Mat & dest, Mat & data, Mat & compressed) const { - data=src.reshape(1,src.rows*src.cols); - compressed=data*proj_matrix; - dest=compressed.reshape(proj_matrix.cols,src.rows).clone(); - } - - /* - * obtain the patch and apply hann window filter to it - */ - bool TrackerKCFImpl::getSubWindow(const Mat img, const Rect _roi, Mat& feat, Mat& patch, TrackerKCF::MODE desc) const { - - Rect region=_roi; - - // return false if roi is outside the image - if((roi & Rect2d(0,0, img.cols, img.rows)) == Rect2d() ) - return false; - - // extract patch inside the image - if(_roi.x<0){region.x=0;region.width+=_roi.x;} - if(_roi.y<0){region.y=0;region.height+=_roi.y;} - if(_roi.x+_roi.width>img.cols)region.width=img.cols-_roi.x; - if(_roi.y+_roi.height>img.rows)region.height=img.rows-_roi.y; - if(region.width>img.cols)region.width=img.cols; - if(region.height>img.rows)region.height=img.rows; - - // return false if region is empty - if (region.empty()) - return false; - - patch=img(region).clone(); - - // add some padding to compensate when the patch is outside image border - int addTop,addBottom, addLeft, addRight; - addTop=region.y-_roi.y; - addBottom=(_roi.height+_roi.y>img.rows?_roi.height+_roi.y-img.rows:0); - addLeft=region.x-_roi.x; - addRight=(_roi.width+_roi.x>img.cols?_roi.width+_roi.x-img.cols:0); - - copyMakeBorder(patch,patch,addTop,addBottom,addLeft,addRight,BORDER_REPLICATE); - if(patch.rows==0 || patch.cols==0)return false; - - // extract the desired descriptors - switch(desc){ - case CN: - CV_Assert(img.channels() == 3); - extractCN(patch,feat); - feat=feat.mul(hann_cn); // hann window filter - break; - default: // GRAY - if(img.channels()>1) - cvtColor(patch,feat, COLOR_BGR2GRAY); - else - feat=patch; - //feat.convertTo(feat,CV_32F); - feat.convertTo(feat,CV_32F, 1.0/255.0, -0.5); - //feat=feat/255.0-0.5; // normalize to range -0.5 .. 0.5 - feat=feat.mul(hann); // hann window filter - break; - } - - return true; - - } - - /* - * get feature using external function - */ - bool TrackerKCFImpl::getSubWindow(const Mat img, const Rect _roi, Mat& feat, void (*f)(const Mat, const Rect, Mat& )) const{ - - // return false if roi is outside the image - if((_roi.x+_roi.width<0) - ||(_roi.y+_roi.height<0) - ||(_roi.x>=img.cols) - ||(_roi.y>=img.rows) - )return false; - - f(img, _roi, feat); - - if(_roi.width != feat.cols || _roi.height != feat.rows){ - printf("error in customized function of features extractor!\n"); - printf("Rules: roi.width==feat.cols && roi.height = feat.rows \n"); - } - - Mat hann_win; - std::vector _layers; - - for(int i=0;i(0,0); - unsigned index; - - if(cnFeatures.type() != CV_32FC(10)) - cnFeatures = Mat::zeros(patch_data.rows,patch_data.cols,CV_32FC(10)); - - for(int i=0;i(i,j); - index=(unsigned)(floor((float)pixel[2]/8)+32*floor((float)pixel[1]/8)+32*32*floor((float)pixel[0]/8)); - - //copy the values - for(int _k=0;_k<10;_k++){ - cnFeatures.at >(i,j)[_k]=ColorNames[index][_k]; - } - } - } - - } - - /* - * dense gauss kernel function - */ - void TrackerKCFImpl::denseGaussKernel(const float sigma, const Mat x_data, const Mat y_data, Mat & k_data, - std::vector & layers_data,std::vector & xf_data,std::vector & yf_data, std::vector xyf_v, Mat xy, Mat xyf ) const { - double normX, normY; - - fft2(x_data,xf_data,layers_data); - fft2(y_data,yf_data,layers_data); - - normX=norm(x_data); - normX*=normX; - normY=norm(y_data); - normY*=normY; - - pixelWiseMult(xf_data,yf_data,xyf_v,0,true); - sumChannels(xyf_v,xyf); - ifft2(xyf,xyf); - - if(params.wrap_kernel){ - shiftRows(xyf, x_data.rows/2); - shiftCols(xyf, x_data.cols/2); + void read(const FileNode& fn) CV_OVERRIDE + { + static_cast(impl.params).read(fn); } - - //(xx + yy - 2 * xy) / numel(x) - xy=(normX+normY-2*xyf)/(x_data.rows*x_data.cols*x_data.channels()); - - // TODO: check wether we really need thresholding or not - //threshold(xy,xy,0.0,0.0,THRESH_TOZERO);//max(0, (xx + yy - 2 * xy) / numel(x)) - for(int i=0;i(i,j)<0.0)xy.at(i,j)=0.0; - } + void write(FileStorage& fs) const CV_OVERRIDE + { + static_cast(impl.params).write(fs); } - float sig=-1.0f/(sigma*sigma); - xy=sig*xy; - exp(xy,k_data); - - } - - /* CIRCULAR SHIFT Function - * http://stackoverflow.com/questions/10420454/shift-like-matlab-function-rows-or-columns-of-a-matrix-in-opencv - */ - // circular shift one row from up to down - void TrackerKCFImpl::shiftRows(Mat& mat) const { - - Mat temp; - Mat m; - int _k = (mat.rows-1); - mat.row(_k).copyTo(temp); - for(; _k > 0 ; _k-- ) { - m = mat.row(_k); - mat.row(_k-1).copyTo(m); - } - m = mat.row(0); - temp.copyTo(m); - - } - - // circular shift n rows from up to down if n > 0, -n rows from down to up if n < 0 - void TrackerKCFImpl::shiftRows(Mat& mat, int n) const { - if( n < 0 ) { - n = -n; - flip(mat,mat,0); - for(int _k=0; _k < n;_k++) { - shiftRows(mat); - } - flip(mat,mat,0); - }else{ - for(int _k=0; _k < n;_k++) { - shiftRows(mat); - } - } - } - - //circular shift n columns from left to right if n > 0, -n columns from right to left if n < 0 - void TrackerKCFImpl::shiftCols(Mat& mat, int n) const { - if(n < 0){ - n = -n; - flip(mat,mat,1); - transpose(mat,mat); - shiftRows(mat,n); - transpose(mat,mat); - flip(mat,mat,1); - }else{ - transpose(mat,mat); - shiftRows(mat,n); - transpose(mat,mat); - } - } - - /* - * calculate the detection response - */ - void TrackerKCFImpl::calcResponse(const Mat alphaf_data, const Mat kf_data, Mat & response_data, Mat & spec_data) const { - //alpha f--> 2channels ; k --> 1 channel; - mulSpectrums(alphaf_data,kf_data,spec_data,0,false); - ifft2(spec_data,response_data); - } - - /* - * calculate the detection response for splitted form - */ - void TrackerKCFImpl::calcResponse(const Mat alphaf_data, const Mat _alphaf_den, const Mat kf_data, Mat & response_data, Mat & spec_data, Mat & spec2_data) const { - - mulSpectrums(alphaf_data,kf_data,spec_data,0,false); - - //z=(a+bi)/(c+di)=[(ac+bd)+i(bc-ad)]/(c^2+d^2) - float den; - for(int i=0;i(i,j)[0]*_alphaf_den.at(i,j)[0]+_alphaf_den.at(i,j)[1]*_alphaf_den.at(i,j)[1]); - spec2_data.at(i,j)[0]= - (spec_data.at(i,j)[0]*_alphaf_den.at(i,j)[0]+spec_data.at(i,j)[1]*_alphaf_den.at(i,j)[1])*den; - spec2_data.at(i,j)[1]= - (spec_data.at(i,j)[1]*_alphaf_den.at(i,j)[0]-spec_data.at(i,j)[0]*_alphaf_den.at(i,j)[1])*den; - } + bool initImpl(const Mat& image, const Rect2d& boundingBox) CV_OVERRIDE + { + impl.init(image, boundingBox); + model = impl.model; + sampler = makePtr(); + featureSet = makePtr(); + isInit = true; + return true; + } + bool updateImpl(const Mat& image, Rect2d& boundingBox) CV_OVERRIDE + { + Rect bb; + bool res = impl.update(image, bb); + boundingBox = bb; + return res; } - - ifft2(spec2_data,response_data); - } - - void TrackerKCFImpl::setFeatureExtractor(void (*f)(const Mat, const Rect, Mat&), bool pca_func){ - if(pca_func){ - extractor_pca.push_back(f); - use_custom_extractor_pca = true; - }else{ - extractor_npca.push_back(f); - use_custom_extractor_npca = true; + void setFeatureExtractor(void (*f)(const Mat, const Rect, Mat&), bool pca_func = false) CV_OVERRIDE + { + impl.setFeatureExtractor(f, pca_func); } - } - /*----------------------------------------------------------------------*/ +}; - /* - * Parameters - */ - TrackerKCF::Params::Params(){ - detect_thresh = 0.5f; - sigma=0.2f; - lambda=0.0001f; - interp_factor=0.075f; - output_sigma_factor=1.0f / 16.0f; - resize=true; - max_patch_size=80*80; - split_coeff=true; - wrap_kernel=false; - desc_npca = GRAY; - desc_pca = CN; +} // namespace - //feature compression - compress_feature=true; - compressed_size=2; - pca_learning_rate=0.15f; - } - - void TrackerKCF::Params::read( const cv::FileNode& fn ){ +void legacy::TrackerKCF::Params::read(const cv::FileNode& fn) +{ *this = TrackerKCF::Params(); if (!fn["detect_thresh"].empty()) @@ -970,9 +138,10 @@ namespace cv{ if (!fn["pca_learning_rate"].empty()) fn["pca_learning_rate"] >> pca_learning_rate; - } +} - void TrackerKCF::Params::write( cv::FileStorage& fs ) const{ +void legacy::TrackerKCF::Params::write(cv::FileStorage& fs) const +{ fs << "detect_thresh" << detect_thresh; fs << "sigma" << sigma; fs << "lambda" << lambda; @@ -987,5 +156,18 @@ namespace cv{ fs << "compress_feature" << compress_feature; fs << "compressed_size" << compressed_size; fs << "pca_learning_rate" << pca_learning_rate; - } -} /* namespace cv */ +} + + +}} // namespace legacy::tracking + +Ptr legacy::TrackerKCF::create(const legacy::TrackerKCF::Params ¶meters) +{ + return makePtr(parameters); +} +Ptr legacy::TrackerKCF::create() +{ + return create(legacy::TrackerKCF::Params()); +} + +} diff --git a/modules/tracking/src/legacy/trackerMIL.legacy.hpp b/modules/tracking/src/legacy/trackerMIL.legacy.hpp index 8ccc3b3b348..8e3a1b4300f 100644 --- a/modules/tracking/src/legacy/trackerMIL.legacy.hpp +++ b/modules/tracking/src/legacy/trackerMIL.legacy.hpp @@ -39,47 +39,54 @@ // //M*/ -#include "precomp.hpp" -#include "trackerMILModel.hpp" +#include "opencv2/tracking/tracking_legacy.hpp" -namespace cv -{ +namespace cv { +namespace legacy { +inline namespace tracking { +namespace impl { -class TrackerMILImpl : public TrackerMIL +class TrackerMILImpl CV_FINAL : public legacy::TrackerMIL { - public: - TrackerMILImpl( const TrackerMIL::Params ¶meters = TrackerMIL::Params() ); - void read( const FileNode& fn ) CV_OVERRIDE; - void write( FileStorage& fs ) const CV_OVERRIDE; - - protected: - - bool initImpl( const Mat& image, const Rect2d& boundingBox ) CV_OVERRIDE; - bool updateImpl( const Mat& image, Rect2d& boundingBox ) CV_OVERRIDE; - void compute_integral( const Mat & img, Mat & ii_img ); - - TrackerMIL::Params params; +public: + cv::tracking::impl::TrackerMILImpl impl; + + TrackerMILImpl(const legacy::TrackerMIL::Params ¶meters) + : impl(parameters) + { + isInit = false; + } + + void read(const FileNode& fn) CV_OVERRIDE + { + static_cast(impl.params).read(fn); + } + void write(FileStorage& fs) const CV_OVERRIDE + { + static_cast(impl.params).write(fs); + } + + bool initImpl(const Mat& image, const Rect2d& boundingBox) CV_OVERRIDE + { + impl.init(image, boundingBox); + model = impl.model; + featureSet = impl.featureSet; + sampler = impl.sampler; + isInit = true; + return true; + } + bool updateImpl(const Mat& image, Rect2d& boundingBox) CV_OVERRIDE + { + Rect bb; + bool res = impl.update(image, bb); + boundingBox = bb; + return res; + } }; -/* - * TrackerMIL - */ - -/* - * Parameters - */ -TrackerMIL::Params::Params() -{ - samplerInitInRadius = 3; - samplerSearchWinSize = 25; - samplerInitMaxNegNum = 65; - samplerTrackInRadius = 4; - samplerTrackMaxPosNum = 100000; - samplerTrackMaxNegNum = 65; - featureSetNumFeatures = 250; -} +} // namespace -void TrackerMIL::Params::read( const cv::FileNode& fn ) +void legacy::TrackerMIL::Params::read(const cv::FileNode& fn) { samplerInitInRadius = fn["samplerInitInRadius"]; samplerSearchWinSize = fn["samplerSearchWinSize"]; @@ -90,7 +97,7 @@ void TrackerMIL::Params::read( const cv::FileNode& fn ) featureSetNumFeatures = fn["featureSetNumFeatures"]; } -void TrackerMIL::Params::write( cv::FileStorage& fs ) const +void legacy::TrackerMIL::Params::write(cv::FileStorage& fs) const { fs << "samplerInitInRadius" << samplerInitInRadius; fs << "samplerSearchWinSize" << samplerSearchWinSize; @@ -99,190 +106,17 @@ void TrackerMIL::Params::write( cv::FileStorage& fs ) const fs << "samplerTrackMaxPosNum" << samplerTrackMaxPosNum; fs << "samplerTrackMaxNegNum" << samplerTrackMaxNegNum; fs << "featureSetNumFeatures" << featureSetNumFeatures; - } -/* - * Constructor - */ -Ptr TrackerMIL::create(const TrackerMIL::Params ¶meters){ - return Ptr(new TrackerMILImpl(parameters)); -} -Ptr TrackerMIL::create(){ - return Ptr(new TrackerMILImpl()); -} -TrackerMILImpl::TrackerMILImpl( const TrackerMIL::Params ¶meters ) : - params( parameters ) -{ - isInit = false; -} +}} // namespace -void TrackerMILImpl::read( const cv::FileNode& fn ) +Ptr legacy::TrackerMIL::create(const legacy::TrackerMIL::Params ¶meters) { - params.read( fn ); -} - -void TrackerMILImpl::write( cv::FileStorage& fs ) const -{ - params.write( fs ); -} - -void TrackerMILImpl::compute_integral( const Mat & img, Mat & ii_img ) -{ - Mat ii; - std::vector ii_imgs; - integral( img, ii, CV_32F ); - split( ii, ii_imgs ); - ii_img = ii_imgs[0]; -} - -bool TrackerMILImpl::initImpl( const Mat& image, const Rect2d& boundingBox ) -{ - srand (1); - Mat intImage; - compute_integral( image, intImage ); - TrackerSamplerCSC::Params CSCparameters; - CSCparameters.initInRad = params.samplerInitInRadius; - CSCparameters.searchWinSize = params.samplerSearchWinSize; - CSCparameters.initMaxNegNum = params.samplerInitMaxNegNum; - CSCparameters.trackInPosRad = params.samplerTrackInRadius; - CSCparameters.trackMaxPosNum = params.samplerTrackMaxPosNum; - CSCparameters.trackMaxNegNum = params.samplerTrackMaxNegNum; - - Ptr CSCSampler = Ptr( new TrackerSamplerCSC( CSCparameters ) ); - if( !sampler->addTrackerSamplerAlgorithm( CSCSampler ) ) - return false; - - //or add CSC sampler with default parameters - //sampler->addTrackerSamplerAlgorithm( "CSC" ); - - //Positive sampling - CSCSampler.staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_POS ); - sampler->sampling( intImage, boundingBox ); - std::vector posSamples = sampler->getSamples(); - - //Negative sampling - CSCSampler.staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_NEG ); - sampler->sampling( intImage, boundingBox ); - std::vector negSamples = sampler->getSamples(); - - if( posSamples.empty() || negSamples.empty() ) - return false; - - //compute HAAR features - TrackerFeatureHAAR::Params HAARparameters; - HAARparameters.numFeatures = params.featureSetNumFeatures; - HAARparameters.rectSize = Size( (int)boundingBox.width, (int)boundingBox.height ); - HAARparameters.isIntegral = true; - Ptr trackerFeature = Ptr( new TrackerFeatureHAAR( HAARparameters ) ); - featureSet->addTrackerFeature( trackerFeature ); - - featureSet->extraction( posSamples ); - const std::vector posResponse = featureSet->getResponses(); - - featureSet->extraction( negSamples ); - const std::vector negResponse = featureSet->getResponses(); - - model = Ptr( new TrackerMILModel( boundingBox ) ); - Ptr stateEstimator = Ptr( - new TrackerStateEstimatorMILBoosting( params.featureSetNumFeatures ) ); - model->setTrackerStateEstimator( stateEstimator ); - - //Run model estimation and update - model.staticCast()->setMode( TrackerMILModel::MODE_POSITIVE, posSamples ); - model->modelEstimation( posResponse ); - model.staticCast()->setMode( TrackerMILModel::MODE_NEGATIVE, negSamples ); - model->modelEstimation( negResponse ); - model->modelUpdate(); - - return true; + return makePtr(parameters); } - -bool TrackerMILImpl::updateImpl( const Mat& image, Rect2d& boundingBox ) +Ptr legacy::TrackerMIL::create() { - Mat intImage; - compute_integral( image, intImage ); - - //get the last location [AAM] X(k-1) - Ptr lastLocation = model->getLastTargetState(); - Rect lastBoundingBox( (int)lastLocation->getTargetPosition().x, (int)lastLocation->getTargetPosition().y, lastLocation->getTargetWidth(), - lastLocation->getTargetHeight() ); - - //sampling new frame based on last location - ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_DETECT ); - sampler->sampling( intImage, lastBoundingBox ); - std::vector detectSamples = sampler->getSamples(); - if( detectSamples.empty() ) - return false; - - /*//TODO debug samples - Mat f; - image.copyTo(f); - - for( size_t i = 0; i < detectSamples.size(); i=i+10 ) - { - Size sz; - Point off; - detectSamples.at(i).locateROI(sz, off); - rectangle(f, Rect(off.x,off.y,detectSamples.at(i).cols,detectSamples.at(i).rows), Scalar(255,0,0), 1); - }*/ - - //extract features from new samples - featureSet->extraction( detectSamples ); - std::vector response = featureSet->getResponses(); - - //predict new location - ConfidenceMap cmap; - model.staticCast()->setMode( TrackerMILModel::MODE_ESTIMATON, detectSamples ); - model.staticCast()->responseToConfidenceMap( response, cmap ); - model->getTrackerStateEstimator().staticCast()->setCurrentConfidenceMap( cmap ); - - if( !model->runStateEstimator() ) - { - return false; - } - - Ptr currentState = model->getLastTargetState(); - boundingBox = Rect( (int)currentState->getTargetPosition().x, (int)currentState->getTargetPosition().y, currentState->getTargetWidth(), - currentState->getTargetHeight() ); - - /*//TODO debug - rectangle(f, lastBoundingBox, Scalar(0,255,0), 1); - rectangle(f, boundingBox, Scalar(0,0,255), 1); - imshow("f", f); - //waitKey( 0 );*/ - - //sampling new frame based on new location - //Positive sampling - ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_POS ); - sampler->sampling( intImage, boundingBox ); - std::vector posSamples = sampler->getSamples(); - - //Negative sampling - ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_NEG ); - sampler->sampling( intImage, boundingBox ); - std::vector negSamples = sampler->getSamples(); - - if( posSamples.empty() || negSamples.empty() ) - return false; - - //extract features - featureSet->extraction( posSamples ); - std::vector posResponse = featureSet->getResponses(); - - featureSet->extraction( negSamples ); - std::vector negResponse = featureSet->getResponses(); - - //model estimate - model.staticCast()->setMode( TrackerMILModel::MODE_POSITIVE, posSamples ); - model->modelEstimation( posResponse ); - model.staticCast()->setMode( TrackerMILModel::MODE_NEGATIVE, negSamples ); - model->modelEstimation( negResponse ); - - //model update - model->modelUpdate(); - - return true; + return create(legacy::TrackerMIL::Params()); } -} /* namespace cv */ +} // namespace diff --git a/modules/tracking/src/mosseTracker.cpp b/modules/tracking/src/mosseTracker.cpp index 038a8ba1d27..b972bf93d5f 100644 --- a/modules/tracking/src/mosseTracker.cpp +++ b/modules/tracking/src/mosseTracker.cpp @@ -13,24 +13,28 @@ // Cracki: for the idea of only converting the used patch to gray // -#include "opencv2/tracking.hpp" +#include "precomp.hpp" + +#include "opencv2/tracking/tracking_legacy.hpp" namespace cv { -namespace tracking { +inline namespace tracking { +namespace impl { +namespace { -struct DummyModel : TrackerModel +struct DummyModel : detail::tracking::TrackerModel { virtual void modelUpdateImpl() CV_OVERRIDE {} virtual void modelEstimationImpl( const std::vector& ) CV_OVERRIDE {} }; - const double eps=0.00001; // for normalization const double rate=0.2; // learning rate const double psrThreshold=5.7; // no detection, if PSR is smaller than this +} // namespace -struct MosseImpl CV_FINAL : TrackerMOSSE +struct MosseImpl CV_FINAL : legacy::TrackerMOSSE { protected: @@ -237,13 +241,12 @@ struct MosseImpl CV_FINAL : TrackerMOSSE }; // MosseImpl -} // tracking - +}} // namespace -Ptr TrackerMOSSE::create() +Ptr legacy::tracking::TrackerMOSSE::create() { - return makePtr(); + return makePtr(); } -} // cv +} // namespace diff --git a/modules/tracking/src/multiTracker.cpp b/modules/tracking/src/multiTracker.cpp index 7b71bccdeb7..963e6eb6579 100644 --- a/modules/tracking/src/multiTracker.cpp +++ b/modules/tracking/src/multiTracker.cpp @@ -39,10 +39,17 @@ // //M*/ +#include "precomp.hpp" #include "multiTracker.hpp" -namespace cv -{ +#include "opencv2/tracking/tracking_legacy.hpp" + +namespace cv { +namespace legacy { +inline namespace tracking { + +using namespace impl; + //Multitracker bool MultiTracker_Alt::addTarget(InputArray image, const Rect2d& boundingBox, Ptr tracker_algorithm) { @@ -249,12 +256,18 @@ namespace cv return success; } +}} // namespace + + +inline namespace tracking { +namespace impl { + void detect_all(const Mat& img, const Mat& imgBlurred, std::vector& res, std::vector < std::vector < tld::TLDDetector::LabeledPatch > > &patches, std::vector &detect_flgs, - std::vector > &trackers) + std::vector > &trackers) { //TLD Tracker data extraction - Tracker* trackerPtr = trackers[0]; - cv::tld::TrackerTLDImpl* tracker = static_cast(trackerPtr); + legacy::Tracker* trackerPtr = trackers[0]; + tld::TrackerTLDImpl* tracker = static_cast(trackerPtr); //TLD Model Extraction tld::TrackerTLDModel* tldModel = ((tld::TrackerTLDModel*)static_cast(tracker->getModel())); Size initSize = tldModel->getMinSize(); @@ -445,11 +458,11 @@ namespace cv #ifdef HAVE_OPENCL void ocl_detect_all(const Mat& img, const Mat& imgBlurred, std::vector& res, std::vector < std::vector < tld::TLDDetector::LabeledPatch > > &patches, std::vector &detect_flgs, - std::vector > &trackers) + std::vector > &trackers) { //TLD Tracker data extraction - Tracker* trackerPtr = trackers[0]; - cv::tld::TrackerTLDImpl* tracker = static_cast(trackerPtr); + legacy::Tracker* trackerPtr = trackers[0]; + tld::TrackerTLDImpl* tracker = static_cast(trackerPtr); //TLD Model Extraction tld::TrackerTLDModel* tldModel = ((tld::TrackerTLDModel*)static_cast(tracker->getModel())); Size initSize = tldModel->getMinSize(); @@ -656,4 +669,4 @@ namespace cv } #endif -} +}}} // namespace diff --git a/modules/tracking/src/multiTracker.hpp b/modules/tracking/src/multiTracker.hpp index 4ab654e9ba2..504fafd22b6 100644 --- a/modules/tracking/src/multiTracker.hpp +++ b/modules/tracking/src/multiTracker.hpp @@ -42,18 +42,18 @@ #ifndef OPENCV_MULTITRACKER #define OPENCV_MULTITRACKER -#include "precomp.hpp" #include "tldTracker.hpp" #include "tldUtils.hpp" #include -namespace cv -{ +namespace cv { +inline namespace tracking { +namespace impl { void detect_all(const Mat& img, const Mat& imgBlurred, std::vector& res, std::vector < std::vector < tld::TLDDetector::LabeledPatch > > &patches, - std::vector& detect_flgs, std::vector >& trackers); + std::vector& detect_flgs, std::vector >& trackers); #ifdef HAVE_OPENCL void ocl_detect_all(const Mat& img, const Mat& imgBlurred, std::vector& res, std::vector < std::vector < tld::TLDDetector::LabeledPatch > > &patches, - std::vector& detect_flgs, std::vector >& trackers); + std::vector& detect_flgs, std::vector >& trackers); #endif -} +}}} // namespace #endif \ No newline at end of file diff --git a/modules/tracking/src/multiTracker_alt.cpp b/modules/tracking/src/multiTracker_alt.cpp index 74974797721..d5a96e7cc42 100644 --- a/modules/tracking/src/multiTracker_alt.cpp +++ b/modules/tracking/src/multiTracker_alt.cpp @@ -40,8 +40,11 @@ //M*/ #include "precomp.hpp" +#include "opencv2/tracking/tracking_legacy.hpp" namespace cv { +namespace legacy { +inline namespace tracking { // constructor MultiTracker::MultiTracker(){}; @@ -105,4 +108,4 @@ namespace cv { return makePtr(); } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/onlineBoosting.cpp b/modules/tracking/src/onlineBoosting.cpp index 3254c0b5c67..800d7ee99d7 100644 --- a/modules/tracking/src/onlineBoosting.cpp +++ b/modules/tracking/src/onlineBoosting.cpp @@ -42,8 +42,9 @@ #include "precomp.hpp" #include "opencv2/tracking/onlineBoosting.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { StrongClassifierDirectSelection::StrongClassifierDirectSelection( int numBaseClf, int numWeakClf, Size patchSz, const Rect& sampleROI, bool useFeatureEx, int iterationInit ) @@ -732,4 +733,4 @@ int ClassifierThreshold::eval( float value ) return ( ( ( m_parity * ( value - m_threshold ) ) > 0 ) ? 1 : -1 ); } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/precomp.hpp b/modules/tracking/src/precomp.hpp index f892f867832..50ebd727224 100644 --- a/modules/tracking/src/precomp.hpp +++ b/modules/tracking/src/precomp.hpp @@ -42,18 +42,29 @@ #ifndef __OPENCV_PRECOMP_H__ #define __OPENCV_PRECOMP_H__ -#include "cvconfig.h" -#include "opencv2/tracking.hpp" -#include "opencv2/core/utility.hpp" +#include "opencv2/core.hpp" #include "opencv2/core/ocl.hpp" -#include #include "opencv2/core/hal/hal.hpp" -namespace cv -{ - extern const float ColorNames[][10]; +#include "opencv2/video/tracking.hpp" + +#include "opencv2/tracking.hpp" + - namespace tracking { +#include "opencv2/tracking/tracking_internals.hpp" + +namespace cv { inline namespace tracking { +namespace impl { } +using namespace impl; +using namespace cv::detail::tracking; +}} // namespace + + +namespace cv { +namespace detail { +inline namespace tracking { + + extern const float ColorNames[][10]; /* Cholesky decomposition The function performs Cholesky decomposition . @@ -102,7 +113,6 @@ namespace cv return success; } - } // tracking -} // cv +}}} // namespace #endif diff --git a/modules/tracking/src/tldDataset.cpp b/modules/tracking/src/tldDataset.cpp index 97b29645fec..6ce0a9785b6 100644 --- a/modules/tracking/src/tldDataset.cpp +++ b/modules/tracking/src/tldDataset.cpp @@ -39,10 +39,13 @@ // //M*/ +#include "precomp.hpp" #include "opencv2/tracking/tldDataset.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { + namespace tld { char tldRootPath[100]; @@ -182,4 +185,5 @@ namespace cv } } -} + +}}} diff --git a/modules/tracking/src/tldDetector.cpp b/modules/tracking/src/tldDetector.cpp index bb52a4899e3..82688213551 100644 --- a/modules/tracking/src/tldDetector.cpp +++ b/modules/tracking/src/tldDetector.cpp @@ -39,15 +39,15 @@ // //M*/ +#include "precomp.hpp" + #include "tldDetector.hpp" #include "tracking_utils.hpp" -#include - -namespace cv -{ - namespace tld - { +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { // Calculate offsets for classifiers void TLDDetector::prepareClassifiers(int rowstep) { @@ -619,5 +619,4 @@ namespace cv return ((p2 - p * p) > VARIANCE_THRESHOLD * *originalVariance); } - } -} +}}}} // namespace diff --git a/modules/tracking/src/tldDetector.hpp b/modules/tracking/src/tldDetector.hpp index 292cbbb356d..a2b329d6e49 100644 --- a/modules/tracking/src/tldDetector.hpp +++ b/modules/tracking/src/tldDetector.hpp @@ -42,15 +42,15 @@ #ifndef OPENCV_TLD_DETECTOR #define OPENCV_TLD_DETECTOR -#include "precomp.hpp" #include "opencl_kernels_tracking.hpp" #include "tldEnsembleClassifier.hpp" #include "tldUtils.hpp" -namespace cv -{ - namespace tld - { +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { + const int STANDARD_PATCH_SIZE = 15; const int NEG_EXAMPLES_IN_INIT_MODEL = 300; const int MAX_EXAMPLES_IN_MODEL = 500; @@ -116,7 +116,6 @@ namespace cv }; - } -} +}}}} // namespace #endif diff --git a/modules/tracking/src/tldEnsembleClassifier.cpp b/modules/tracking/src/tldEnsembleClassifier.cpp index 1d72b3e81d4..c800fc3c9fc 100644 --- a/modules/tracking/src/tldEnsembleClassifier.cpp +++ b/modules/tracking/src/tldEnsembleClassifier.cpp @@ -39,12 +39,14 @@ // //M*/ +#include "precomp.hpp" #include "tldEnsembleClassifier.hpp" -namespace cv -{ - namespace tld - { +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { + // Constructor TLDEnsembleClassifier::TLDEnsembleClassifier(const std::vector& meas, int beg, int end) :lastStep_(-1) { @@ -194,5 +196,4 @@ namespace cv return (int)classifiers.size(); } - } -} \ No newline at end of file +}}}} // namespace diff --git a/modules/tracking/src/tldEnsembleClassifier.hpp b/modules/tracking/src/tldEnsembleClassifier.hpp index f0ec175bad7..6df65e4814d 100644 --- a/modules/tracking/src/tldEnsembleClassifier.hpp +++ b/modules/tracking/src/tldEnsembleClassifier.hpp @@ -40,12 +40,12 @@ //M*/ #include -#include "precomp.hpp" -namespace cv -{ - namespace tld - { +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { + class TLDEnsembleClassifier { public: @@ -64,5 +64,5 @@ namespace cv std::vector offset; int lastStep_; }; - } -} \ No newline at end of file + +}}}} // namespace diff --git a/modules/tracking/src/tldModel.cpp b/modules/tracking/src/tldModel.cpp index 833e586d0c8..ad787ab8e01 100644 --- a/modules/tracking/src/tldModel.cpp +++ b/modules/tracking/src/tldModel.cpp @@ -39,14 +39,14 @@ // //M*/ +#include "precomp.hpp" #include "tldModel.hpp" -#include +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { -namespace cv -{ - namespace tld - { //Constructor TrackerTLDModel::TrackerTLDModel(TrackerTLD::Params params, const Mat& image, const Rect2d& boundingBox, Size minSize): timeStampPositiveNext(0), timeStampNegativeNext(0), minSize_(minSize), params_(params), boundingBox_(boundingBox) @@ -361,5 +361,5 @@ namespace cv dfprintf((port, "\tpositiveExamples.size() = %d\n", (int)positiveExamples.size())); dfprintf((port, "\tnegativeExamples.size() = %d\n", (int)negativeExamples.size())); } - } -} + +}}}} // namespace diff --git a/modules/tracking/src/tldModel.hpp b/modules/tracking/src/tldModel.hpp index 7616ebdc439..57331756cb0 100644 --- a/modules/tracking/src/tldModel.hpp +++ b/modules/tracking/src/tldModel.hpp @@ -45,10 +45,15 @@ #include "tldDetector.hpp" #include "tldUtils.hpp" -namespace cv -{ - namespace tld - { +#include "opencv2/tracking/tracking_legacy.hpp" + +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { + +using namespace cv::legacy; + class TrackerTLDModel : public TrackerModel { public: @@ -84,7 +89,6 @@ namespace cv RNG rng; }; - } -} +}}}} // namespace #endif diff --git a/modules/tracking/src/tldTracker.cpp b/modules/tracking/src/tldTracker.cpp index db1fccf5498..92038ef2d5e 100644 --- a/modules/tracking/src/tldTracker.cpp +++ b/modules/tracking/src/tldTracker.cpp @@ -39,10 +39,15 @@ // //M*/ +#include "precomp.hpp" +#include "opencv2/tracking/tracking_legacy.hpp" #include "tldTracker.hpp" -namespace cv -{ +namespace cv { +namespace legacy { +inline namespace tracking { +using namespace impl; +using namespace impl::tld; TrackerTLD::Params::Params(){} @@ -60,8 +65,11 @@ Ptr TrackerTLD::create() return Ptr(new tld::TrackerTLDImpl()); } -namespace tld -{ +}} // namespace + +inline namespace tracking { +namespace impl { +namespace tld { TrackerTLDImpl::TrackerTLDImpl(const TrackerTLD::Params ¶meters) : params( parameters ) @@ -323,6 +331,4 @@ void Data::printme(FILE* port) dfprintf((port, "\tminSize = %dx%d\n", minSize.width, minSize.height)); } -} - -} +}}}} // namespace diff --git a/modules/tracking/src/tldTracker.hpp b/modules/tracking/src/tldTracker.hpp index 3f9861192cc..e96b3e04d34 100644 --- a/modules/tracking/src/tldTracker.hpp +++ b/modules/tracking/src/tldTracker.hpp @@ -42,18 +42,17 @@ #ifndef OPENCV_TLD_TRACKER #define OPENCV_TLD_TRACKER -#include "precomp.hpp" #include "opencv2/video/tracking.hpp" #include "opencv2/imgproc.hpp" #include "tldModel.hpp" -#include -#include +#include +#include -namespace cv -{ +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { -namespace tld -{ class TrackerProxy { public: @@ -168,7 +167,6 @@ class TrackerTLDImpl : public TrackerTLD }; -} -} +}}}} // namespace #endif diff --git a/modules/tracking/src/tldUtils.cpp b/modules/tracking/src/tldUtils.cpp index b570a1f3bb9..5dca7b5f855 100644 --- a/modules/tracking/src/tldUtils.cpp +++ b/modules/tracking/src/tldUtils.cpp @@ -39,13 +39,14 @@ // //M*/ +#include "precomp.hpp" #include "tldUtils.hpp" -namespace cv -{ -namespace tld -{ +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { //Debug functions and variables Rect2d etalon(14.0, 110.0, 20.0, 20.0); @@ -192,4 +193,4 @@ void resample(const Mat& img, const Rect2d& r2, Mat_& samples) } -}} +}}}} // namespace diff --git a/modules/tracking/src/tldUtils.hpp b/modules/tracking/src/tldUtils.hpp index 1a606429d28..311e08f5ed4 100644 --- a/modules/tracking/src/tldUtils.hpp +++ b/modules/tracking/src/tldUtils.hpp @@ -1,12 +1,11 @@ #ifndef OPENCV_TLD_UTILS #define OPENCV_TLD_UTILS -#include "precomp.hpp" +namespace cv { +inline namespace tracking { +namespace impl { +namespace tld { -namespace cv -{ - namespace tld - { //debug functions and variables #define ALEX_DEBUG #ifdef ALEX_DEBUG @@ -48,7 +47,7 @@ namespace cv double variance(const Mat& img); void getClosestN(std::vector& scanGrid, Rect2d bBox, int n, std::vector& res); double scaleAndBlur(const Mat& originalImg, int scale, Mat& scaledImg, Mat& blurredImg, Size GaussBlurKernelSize, double scaleStep); - } -} + +}}}} // namespace #endif diff --git a/modules/tracking/src/tracker.cpp b/modules/tracking/src/tracker.cpp new file mode 100644 index 00000000000..d6c0a8d0a61 --- /dev/null +++ b/modules/tracking/src/tracker.cpp @@ -0,0 +1,23 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "precomp.hpp" + +namespace cv { +inline namespace tracking { + +Tracker::Tracker() +{ + // nothing +} + +Tracker::~Tracker() +{ + // nothing +} + +}} // namespace + + +#include "legacy/tracker.legacy.hpp" diff --git a/modules/tracking/src/trackerBoosting.cpp b/modules/tracking/src/trackerBoosting.cpp index 05b11aa2d59..e3dcc1998f3 100644 --- a/modules/tracking/src/trackerBoosting.cpp +++ b/modules/tracking/src/trackerBoosting.cpp @@ -42,8 +42,12 @@ #include "precomp.hpp" #include "trackerBoostingModel.hpp" -namespace cv -{ +#include "opencv2/tracking/tracking_legacy.hpp" + +namespace cv { +namespace legacy { +inline namespace tracking { +using namespace impl; class TrackerBoostingImpl : public TrackerBoosting { @@ -319,4 +323,4 @@ bool TrackerBoostingImpl::updateImpl( const Mat& image, Rect2d& boundingBox ) return true; } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/trackerBoostingModel.cpp b/modules/tracking/src/trackerBoostingModel.cpp index bd6148a9dbf..3fb48f0959e 100644 --- a/modules/tracking/src/trackerBoostingModel.cpp +++ b/modules/tracking/src/trackerBoostingModel.cpp @@ -39,14 +39,16 @@ // //M*/ +#include "precomp.hpp" #include "trackerBoostingModel.hpp" /** * TrackerBoostingModel */ -namespace cv -{ +namespace cv { +inline namespace tracking { +namespace impl { TrackerBoostingModel::TrackerBoostingModel( const Rect& boundingBox ) { @@ -119,4 +121,4 @@ void TrackerBoostingModel::responseToConfidenceMap( const std::vector& resp } } -} +}}} // namespace diff --git a/modules/tracking/src/trackerBoostingModel.hpp b/modules/tracking/src/trackerBoostingModel.hpp index 0d4582cd16a..45aa1dc6dfb 100644 --- a/modules/tracking/src/trackerBoostingModel.hpp +++ b/modules/tracking/src/trackerBoostingModel.hpp @@ -42,11 +42,9 @@ #ifndef __OPENCV_TRACKER_BOOSTING_MODEL_HPP__ #define __OPENCV_TRACKER_BOOSTING_MODEL_HPP__ -#include "precomp.hpp" -#include "opencv2/core.hpp" - -namespace cv -{ +namespace cv { +inline namespace tracking { +namespace impl { /** * \brief Implementation of TrackerModel for BOOSTING algorithm @@ -103,6 +101,6 @@ class TrackerBoostingModel : public TrackerModel int mode; }; -} /* namespace cv */ +}}} // namespace #endif diff --git a/modules/tracking/src/trackerCSRT.cpp b/modules/tracking/src/trackerCSRT.cpp new file mode 100644 index 00000000000..1b3f48aa714 --- /dev/null +++ b/modules/tracking/src/trackerCSRT.cpp @@ -0,0 +1,655 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "precomp.hpp" + +#include "trackerCSRTSegmentation.hpp" +#include "trackerCSRTUtils.hpp" +#include "trackerCSRTScaleEstimation.hpp" + +namespace cv { +inline namespace tracking { +namespace impl { + +/** +* \brief Implementation of TrackerModel for CSRT algorithm +*/ +class TrackerCSRTModel CV_FINAL : public TrackerModel +{ +public: + TrackerCSRTModel(){} + ~TrackerCSRTModel(){} +protected: + void modelEstimationImpl(const std::vector& /*responses*/) CV_OVERRIDE {} + void modelUpdateImpl() CV_OVERRIDE {} +}; + +class TrackerCSRTImpl CV_FINAL : public TrackerCSRT +{ +public: + TrackerCSRTImpl(const Params ¶meters = Params()); + + Params params; + + Ptr model; + + // Tracker API + virtual void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + virtual bool update(InputArray image, Rect& boundingBox) CV_OVERRIDE; + virtual void setInitialMask(InputArray mask) CV_OVERRIDE; + +protected: + void update_csr_filter(const Mat &image, const Mat &my_mask); + void update_histograms(const Mat &image, const Rect ®ion); + void extract_histograms(const Mat &image, cv::Rect region, Histogram &hf, Histogram &hb); + std::vector create_csr_filter(const std::vector + img_features, const cv::Mat Y, const cv::Mat P); + Mat calculate_response(const Mat &image, const std::vector filter); + Mat get_location_prior(const Rect roi, const Size2f target_size, const Size img_sz); + Mat segment_region(const Mat &image, const Point2f &object_center, + const Size2f &template_size, const Size &target_size, float scale_factor); + Point2f estimate_new_position(const Mat &image); + std::vector get_features(const Mat &patch, const Size2i &feature_size); + + bool check_mask_area(const Mat &mat, const double obj_area); + float current_scale_factor; + Mat window; + Mat yf; + Rect2f bounding_box; + std::vector csr_filter; + std::vector filter_weights; + Size2f original_target_size; + Size2i image_size; + Size2f template_size; + Size2i rescaled_template_size; + float rescale_ratio; + Point2f object_center; + DSST dsst; + Histogram hist_foreground; + Histogram hist_background; + double p_b; + Mat erode_element; + Mat filter_mask; + Mat preset_mask; + Mat default_mask; + float default_mask_area; + int cell_size; +}; + +TrackerCSRTImpl::TrackerCSRTImpl(const TrackerCSRT::Params ¶meters) : + params(parameters) +{ + // nothing +} + +void TrackerCSRTImpl::setInitialMask(InputArray mask) +{ + preset_mask = mask.getMat(); +} + +bool TrackerCSRTImpl::check_mask_area(const Mat &mat, const double obj_area) +{ + double threshold = 0.05; + double mask_area= sum(mat)[0]; + if(mask_area < threshold*obj_area) { + return false; + } + return true; +} + +Mat TrackerCSRTImpl::calculate_response(const Mat &image, const std::vector filter) +{ + Mat patch = get_subwindow(image, object_center, cvFloor(current_scale_factor * template_size.width), + cvFloor(current_scale_factor * template_size.height)); + resize(patch, patch, rescaled_template_size, 0, 0, INTER_CUBIC); + + std::vector ftrs = get_features(patch, yf.size()); + std::vector Ffeatures = fourier_transform_features(ftrs); + Mat resp, res; + if(params.use_channel_weights){ + res = Mat::zeros(Ffeatures[0].size(), CV_32FC2); + Mat resp_ch; + Mat mul_mat; + for(size_t i = 0; i < Ffeatures.size(); ++i) { + mulSpectrums(Ffeatures[i], filter[i], resp_ch, 0, true); + res += (resp_ch * filter_weights[i]); + } + idft(res, res, DFT_SCALE | DFT_REAL_OUTPUT); + } else { + res = Mat::zeros(Ffeatures[0].size(), CV_32FC2); + Mat resp_ch; + for(size_t i = 0; i < Ffeatures.size(); ++i) { + mulSpectrums(Ffeatures[i], filter[i], resp_ch, 0 , true); + res = res + resp_ch; + } + idft(res, res, DFT_SCALE | DFT_REAL_OUTPUT); + } + return res; +} + +void TrackerCSRTImpl::update_csr_filter(const Mat &image, const Mat &mask) +{ + Mat patch = get_subwindow(image, object_center, cvFloor(current_scale_factor * template_size.width), + cvFloor(current_scale_factor * template_size.height)); + resize(patch, patch, rescaled_template_size, 0, 0, INTER_CUBIC); + + std::vector ftrs = get_features(patch, yf.size()); + std::vector Fftrs = fourier_transform_features(ftrs); + std::vector new_csr_filter = create_csr_filter(Fftrs, yf, mask); + //calculate per channel weights + if(params.use_channel_weights) { + Mat current_resp; + double max_val; + float sum_weights = 0; + std::vector new_filter_weights = std::vector(new_csr_filter.size()); + for(size_t i = 0; i < new_csr_filter.size(); ++i) { + mulSpectrums(Fftrs[i], new_csr_filter[i], current_resp, 0, true); + idft(current_resp, current_resp, DFT_SCALE | DFT_REAL_OUTPUT); + minMaxLoc(current_resp, NULL, &max_val, NULL, NULL); + sum_weights += static_cast(max_val); + new_filter_weights[i] = static_cast(max_val); + } + //update filter weights with new values + float updated_sum = 0; + for(size_t i = 0; i < filter_weights.size(); ++i) { + filter_weights[i] = filter_weights[i]*(1.0f - params.weights_lr) + + params.weights_lr * (new_filter_weights[i] / sum_weights); + updated_sum += filter_weights[i]; + } + //normalize weights + for(size_t i = 0; i < filter_weights.size(); ++i) { + filter_weights[i] /= updated_sum; + } + } + for(size_t i = 0; i < csr_filter.size(); ++i) { + csr_filter[i] = (1.0f - params.filter_lr)*csr_filter[i] + params.filter_lr * new_csr_filter[i]; + } + std::vector().swap(ftrs); + std::vector().swap(Fftrs); +} + + +std::vector TrackerCSRTImpl::get_features(const Mat &patch, const Size2i &feature_size) +{ + std::vector features; + if (params.use_hog) { + std::vector hog = get_features_hog(patch, cell_size); + features.insert(features.end(), hog.begin(), + hog.begin()+params.num_hog_channels_used); + } + if (params.use_color_names) { + std::vector cn; + cn = get_features_cn(patch, feature_size); + features.insert(features.end(), cn.begin(), cn.end()); + } + if(params.use_gray) { + Mat gray_m; + cvtColor(patch, gray_m, COLOR_BGR2GRAY); + resize(gray_m, gray_m, feature_size, 0, 0, INTER_CUBIC); + gray_m.convertTo(gray_m, CV_32FC1, 1.0/255.0, -0.5); + features.push_back(gray_m); + } + if(params.use_rgb) { + std::vector rgb_features = get_features_rgb(patch, feature_size); + features.insert(features.end(), rgb_features.begin(), rgb_features.end()); + } + + for (size_t i = 0; i < features.size(); ++i) { + features.at(i) = features.at(i).mul(window); + } + return features; +} + +class ParallelCreateCSRFilter : public ParallelLoopBody { +public: + ParallelCreateCSRFilter( + const std::vector img_features, + const cv::Mat Y, + const cv::Mat P, + int admm_iterations, + std::vector &result_filter_): + result_filter(result_filter_) + { + this->img_features = img_features; + this->Y = Y; + this->P = P; + this->admm_iterations = admm_iterations; + } + virtual void operator ()(const Range& range) const CV_OVERRIDE + { + for (int i = range.start; i < range.end; i++) { + float mu = 5.0f; + float beta = 3.0f; + float mu_max = 20.0f; + float lambda = mu / 100.0f; + + Mat F = img_features[i]; + + Mat Sxy, Sxx; + mulSpectrums(F, Y, Sxy, 0, true); + mulSpectrums(F, F, Sxx, 0, true); + + Mat H; + H = divide_complex_matrices(Sxy, (Sxx + lambda)); + idft(H, H, DFT_SCALE|DFT_REAL_OUTPUT); + H = H.mul(P); + dft(H, H, DFT_COMPLEX_OUTPUT); + Mat L = Mat::zeros(H.size(), H.type()); //Lagrangian multiplier + Mat G; + for(int iteration = 0; iteration < admm_iterations; ++iteration) { + G = divide_complex_matrices((Sxy + (mu * H) - L) , (Sxx + mu)); + idft((mu * G) + L, H, DFT_SCALE | DFT_REAL_OUTPUT); + float lm = 1.0f / (lambda+mu); + H = H.mul(P*lm); + dft(H, H, DFT_COMPLEX_OUTPUT); + + //Update variables for next iteration + L = L + mu * (G - H); + mu = min(mu_max, beta*mu); + } + result_filter[i] = H; + } + } + + ParallelCreateCSRFilter& operator=(const ParallelCreateCSRFilter &) { + return *this; + } + +private: + int admm_iterations; + Mat Y; + Mat P; + std::vector img_features; + std::vector &result_filter; +}; + + +std::vector TrackerCSRTImpl::create_csr_filter( + const std::vector img_features, + const cv::Mat Y, + const cv::Mat P) +{ + std::vector result_filter; + result_filter.resize(img_features.size()); + ParallelCreateCSRFilter parallelCreateCSRFilter(img_features, Y, P, + params.admm_iterations, result_filter); + parallel_for_(Range(0, static_cast(result_filter.size())), parallelCreateCSRFilter); + + return result_filter; +} + +Mat TrackerCSRTImpl::get_location_prior( + const Rect roi, + const Size2f target_size, + const Size img_sz) +{ + int x1 = cvRound(max(min(roi.x-1, img_sz.width-1) , 0)); + int y1 = cvRound(max(min(roi.y-1, img_sz.height-1) , 0)); + + int x2 = cvRound(min(max(roi.width-1, 0) , img_sz.width-1)); + int y2 = cvRound(min(max(roi.height-1, 0) , img_sz.height-1)); + + Size target_sz; + target_sz.width = target_sz.height = cvFloor(min(target_size.width, target_size.height)); + + double cx = x1 + (x2-x1)/2.; + double cy = y1 + (y2-y1)/2.; + double kernel_size_width = 1.0/(0.5*static_cast(target_sz.width)*1.4142+1); + double kernel_size_height = 1.0/(0.5*static_cast(target_sz.height)*1.4142+1); + + cv::Mat kernel_weight = Mat::zeros(1 + cvFloor(y2 - y1) , 1+cvFloor(-(x1-cx) + (x2-cx)), CV_64FC1); + for (int y = y1; y < y2+1; ++y){ + double * weightPtr = kernel_weight.ptr(y); + double tmp_y = std::pow((cy-y)*kernel_size_height, 2); + for (int x = x1; x < x2+1; ++x){ + weightPtr[x] = kernel_epan(std::pow((cx-x)*kernel_size_width,2) + tmp_y); + } + } + + double max_val; + cv::minMaxLoc(kernel_weight, NULL, &max_val, NULL, NULL); + Mat fg_prior = kernel_weight / max_val; + fg_prior.setTo(0.5, fg_prior < 0.5); + fg_prior.setTo(0.9, fg_prior > 0.9); + return fg_prior; +} + +Mat TrackerCSRTImpl::segment_region( + const Mat &image, + const Point2f &object_center, + const Size2f &template_size, + const Size &target_size, + float scale_factor) +{ + Rect valid_pixels; + Mat patch = get_subwindow(image, object_center, cvFloor(scale_factor * template_size.width), + cvFloor(scale_factor * template_size.height), &valid_pixels); + Size2f scaled_target = Size2f(target_size.width * scale_factor, + target_size.height * scale_factor); + Mat fg_prior = get_location_prior( + Rect(0,0, patch.size().width, patch.size().height), + scaled_target , patch.size()); + + std::vector img_channels; + split(patch, img_channels); + std::pair probs = Segment::computePosteriors2(img_channels, 0, 0, patch.cols, patch.rows, + p_b, fg_prior, 1.0-fg_prior, hist_foreground, hist_background); + + Mat mask = Mat::zeros(probs.first.size(), probs.first.type()); + probs.first(valid_pixels).copyTo(mask(valid_pixels)); + double max_resp = get_max(mask); + threshold(mask, mask, max_resp / 2.0, 1, THRESH_BINARY); + mask.convertTo(mask, CV_32FC1, 1.0); + return mask; +} + + +void TrackerCSRTImpl::extract_histograms(const Mat &image, cv::Rect region, Histogram &hf, Histogram &hb) +{ + // get coordinates of the region + int x1 = std::min(std::max(0, region.x), image.cols-1); + int y1 = std::min(std::max(0, region.y), image.rows-1); + int x2 = std::min(std::max(0, region.x + region.width), image.cols-1); + int y2 = std::min(std::max(0, region.y + region.height), image.rows-1); + + // calculate coordinates of the background region + int offsetX = (x2-x1+1) / params.background_ratio; + int offsetY = (y2-y1+1) / params.background_ratio; + int outer_y1 = std::max(0, (int)(y1-offsetY)); + int outer_y2 = std::min(image.rows, (int)(y2+offsetY+1)); + int outer_x1 = std::max(0, (int)(x1-offsetX)); + int outer_x2 = std::min(image.cols, (int)(x2+offsetX+1)); + + // calculate probability for the background + p_b = 1.0 - ((x2-x1+1) * (y2-y1+1)) / + ((double) (outer_x2-outer_x1+1) * (outer_y2-outer_y1+1)); + + // split multi-channel image into the std::vector of matrices + std::vector img_channels(image.channels()); + split(image, img_channels); + for(size_t k=0; k().swap(img_channels); +} + +void TrackerCSRTImpl::update_histograms(const Mat &image, const Rect ®ion) +{ + // create temporary histograms + Histogram hf(image.channels(), params.histogram_bins); + Histogram hb(image.channels(), params.histogram_bins); + extract_histograms(image, region, hf, hb); + + // get histogram vectors from temporary histograms + std::vector hf_vect_new = hf.getHistogramVector(); + std::vector hb_vect_new = hb.getHistogramVector(); + // get histogram vectors from learned histograms + std::vector hf_vect = hist_foreground.getHistogramVector(); + std::vector hb_vect = hist_background.getHistogramVector(); + + // update histograms - use learning rate + for(size_t i=0; i().swap(hf_vect); + std::vector().swap(hb_vect); +} + +Point2f TrackerCSRTImpl::estimate_new_position(const Mat &image) +{ + + Mat resp = calculate_response(image, csr_filter); + + double max_val; + Point max_loc; + minMaxLoc(resp, NULL, &max_val, NULL, &max_loc); + if (max_val < params.psr_threshold) + return Point2f(-1,-1); // target "lost" + + // take into account also subpixel accuracy + float col = ((float) max_loc.x) + subpixel_peak(resp, "horizontal", max_loc); + float row = ((float) max_loc.y) + subpixel_peak(resp, "vertical", max_loc); + if(row + 1 > (float)resp.rows / 2.0f) { + row = row - resp.rows; + } + if(col + 1 > (float)resp.cols / 2.0f) { + col = col - resp.cols; + } + // calculate x and y displacements + Point2f new_center = object_center + Point2f(current_scale_factor * (1.0f / rescale_ratio) *cell_size*(col), + current_scale_factor * (1.0f / rescale_ratio) *cell_size*(row)); + //sanity checks + if(new_center.x < 0) + new_center.x = 0; + if(new_center.x >= image_size.width) + new_center.x = static_cast(image_size.width - 1); + if(new_center.y < 0) + new_center.y = 0; + if(new_center.y >= image_size.height) + new_center.y = static_cast(image_size.height - 1); + + return new_center; +} + +// ********************************************************************* +// * Update API function * +// ********************************************************************* +bool TrackerCSRTImpl::update(InputArray image_, Rect& boundingBox) +{ + Mat image; + if(image_.channels() == 1) //treat gray image as color image + cvtColor(image_, image, COLOR_GRAY2BGR); + else + image = image_.getMat(); + + object_center = estimate_new_position(image); + if (object_center.x < 0 && object_center.y < 0) + return false; + + current_scale_factor = dsst.getScale(image, object_center); + //update bouding_box according to new scale and location + bounding_box.x = object_center.x - current_scale_factor * original_target_size.width / 2.0f; + bounding_box.y = object_center.y - current_scale_factor * original_target_size.height / 2.0f; + bounding_box.width = current_scale_factor * original_target_size.width; + bounding_box.height = current_scale_factor * original_target_size.height; + + //update tracker + if(params.use_segmentation) { + Mat hsv_img = bgr2hsv(image); + update_histograms(hsv_img, bounding_box); + filter_mask = segment_region(hsv_img, object_center, + template_size,original_target_size, current_scale_factor); + resize(filter_mask, filter_mask, yf.size(), 0, 0, INTER_NEAREST); + if(check_mask_area(filter_mask, default_mask_area)) { + dilate(filter_mask , filter_mask, erode_element); + } else { + filter_mask = default_mask; + } + } else { + filter_mask = default_mask; + } + update_csr_filter(image, filter_mask); + dsst.update(image, object_center); + boundingBox = bounding_box; + return true; +} + +// ********************************************************************* +// * Init API function * +// ********************************************************************* +void TrackerCSRTImpl::init(InputArray image_, const Rect& boundingBox) +{ + Mat image; + if(image_.channels() == 1) //treat gray image as color image + cvtColor(image_, image, COLOR_GRAY2BGR); + else + image = image_.getMat(); + + current_scale_factor = 1.0; + image_size = image.size(); + bounding_box = boundingBox; + cell_size = cvFloor(std::min(4.0, std::max(1.0, static_cast( + cvCeil((bounding_box.width * bounding_box.height)/400.0))))); + original_target_size = Size(bounding_box.size()); + + template_size.width = static_cast(cvFloor(original_target_size.width + params.padding * + sqrt(original_target_size.width * original_target_size.height))); + template_size.height = static_cast(cvFloor(original_target_size.height + params.padding * + sqrt(original_target_size.width * original_target_size.height))); + template_size.width = template_size.height = + (template_size.width + template_size.height) / 2.0f; + rescale_ratio = sqrt(pow(params.template_size,2) / (template_size.width * template_size.height)); + if(rescale_ratio > 1) { + rescale_ratio = 1; + } + rescaled_template_size = Size2i(cvFloor(template_size.width * rescale_ratio), + cvFloor(template_size.height * rescale_ratio)); + object_center = Point2f(static_cast(boundingBox.x) + original_target_size.width / 2.0f, + static_cast(boundingBox.y) + original_target_size.height / 2.0f); + + yf = gaussian_shaped_labels(params.gsl_sigma, + rescaled_template_size.width / cell_size, rescaled_template_size.height / cell_size); + if(params.window_function.compare("hann") == 0) { + window = get_hann_win(Size(yf.cols,yf.rows)); + } else if(params.window_function.compare("cheb") == 0) { + window = get_chebyshev_win(Size(yf.cols,yf.rows), params.cheb_attenuation); + } else if(params.window_function.compare("kaiser") == 0) { + window = get_kaiser_win(Size(yf.cols,yf.rows), params.kaiser_alpha); + } else { + CV_Error(Error::StsBadArg, "Not a valid window function"); + } + + Size2i scaled_obj_size = Size2i(cvFloor(original_target_size.width * rescale_ratio / cell_size), + cvFloor(original_target_size.height * rescale_ratio / cell_size)); + //set dummy mask and area; + int x0 = std::max((yf.size().width - scaled_obj_size.width)/2 - 1, 0); + int y0 = std::max((yf.size().height - scaled_obj_size.height)/2 - 1, 0); + default_mask = Mat::zeros(yf.size(), CV_32FC1); + default_mask(Rect(x0,y0,scaled_obj_size.width, scaled_obj_size.height)) = 1.0f; + default_mask_area = static_cast(sum(default_mask)[0]); + + //initalize segmentation + if(params.use_segmentation) { + Mat hsv_img = bgr2hsv(image); + hist_foreground = Histogram(hsv_img.channels(), params.histogram_bins); + hist_background = Histogram(hsv_img.channels(), params.histogram_bins); + extract_histograms(hsv_img, bounding_box, hist_foreground, hist_background); + filter_mask = segment_region(hsv_img, object_center, template_size, + original_target_size, current_scale_factor); + //update calculated mask with preset mask + if(preset_mask.data){ + Mat preset_mask_padded = Mat::zeros(filter_mask.size(), filter_mask.type()); + int sx = std::max((int)cvFloor(preset_mask_padded.cols / 2.0f - preset_mask.cols / 2.0f) - 1, 0); + int sy = std::max((int)cvFloor(preset_mask_padded.rows / 2.0f - preset_mask.rows / 2.0f) - 1, 0); + preset_mask.copyTo(preset_mask_padded( + Rect(sx, sy, preset_mask.cols, preset_mask.rows))); + filter_mask = filter_mask.mul(preset_mask_padded); + } + erode_element = getStructuringElement(MORPH_ELLIPSE, Size(3,3), Point(1,1)); + resize(filter_mask, filter_mask, yf.size(), 0, 0, INTER_NEAREST); + if(check_mask_area(filter_mask, default_mask_area)) { + dilate(filter_mask , filter_mask, erode_element); + } else { + filter_mask = default_mask; + } + + } else { + filter_mask = default_mask; + } + + //initialize filter + Mat patch = get_subwindow(image, object_center, cvFloor(current_scale_factor * template_size.width), + cvFloor(current_scale_factor * template_size.height)); + resize(patch, patch, rescaled_template_size, 0, 0, INTER_CUBIC); + std::vector patch_ftrs = get_features(patch, yf.size()); + std::vector Fftrs = fourier_transform_features(patch_ftrs); + csr_filter = create_csr_filter(Fftrs, yf, filter_mask); + + if(params.use_channel_weights) { + Mat current_resp; + filter_weights = std::vector(csr_filter.size()); + float chw_sum = 0; + for (size_t i = 0; i < csr_filter.size(); ++i) { + mulSpectrums(Fftrs[i], csr_filter[i], current_resp, 0, true); + idft(current_resp, current_resp, DFT_SCALE | DFT_REAL_OUTPUT); + double max_val; + minMaxLoc(current_resp, NULL, &max_val, NULL , NULL); + chw_sum += static_cast(max_val); + filter_weights[i] = static_cast(max_val); + } + for (size_t i = 0; i < filter_weights.size(); ++i) { + filter_weights[i] /= chw_sum; + } + } + + //initialize scale search + dsst = DSST(image, bounding_box, template_size, params.number_of_scales, params.scale_step, + params.scale_model_max_area, params.scale_sigma_factor, params.scale_lr); + + model=makePtr(); +} + +} // namespace impl + +TrackerCSRT::Params::Params() +{ + use_channel_weights = true; + use_segmentation = true; + use_hog = true; + use_color_names = true; + use_gray = true; + use_rgb = false; + window_function = "hann"; + kaiser_alpha = 3.75f; + cheb_attenuation = 45; + padding = 3.0f; + template_size = 200; + gsl_sigma = 1.0f; + hog_orientations = 9; + hog_clip = 0.2f; + num_hog_channels_used = 18; + filter_lr = 0.02f; + weights_lr = 0.02f; + admm_iterations = 4; + number_of_scales = 33; + scale_sigma_factor = 0.250f; + scale_model_max_area = 512.0f; + scale_lr = 0.025f; + scale_step = 1.020f; + histogram_bins = 16; + background_ratio = 2; + histogram_lr = 0.04f; + psr_threshold = 0.035f; +} + +TrackerCSRT::TrackerCSRT() +{ + // nothing +} + +TrackerCSRT::~TrackerCSRT() +{ + // nothing +} + +Ptr TrackerCSRT::create(const TrackerCSRT::Params ¶meters) +{ + return makePtr(parameters); +} + +}} // namespace + +#include "legacy/trackerCSRT.legacy.hpp" diff --git a/modules/tracking/src/trackerCSRTScaleEstimation.cpp b/modules/tracking/src/trackerCSRTScaleEstimation.cpp index 5bde5f420b7..221d1ee6413 100644 --- a/modules/tracking/src/trackerCSRTScaleEstimation.cpp +++ b/modules/tracking/src/trackerCSRTScaleEstimation.cpp @@ -127,7 +127,7 @@ DSST::DSST(const Mat &image, mulSpectrums(ysf, Fscale_resp, sf_num, 0 , true); Mat sf_den_all; mulSpectrums(Fscale_resp, Fscale_resp, sf_den_all, 0, true); - reduce(sf_den_all, sf_den, 0, CV_REDUCE_SUM, -1); + reduce(sf_den_all, sf_den, 0, REDUCE_SUM, -1); } DSST::~DSST() @@ -178,7 +178,7 @@ void DSST::update(const Mat &image, const Point2f object_center) mulSpectrums(ysf, Fscale_features, new_sf_num, DFT_ROWS, true); Mat sf_den_all; mulSpectrums(Fscale_features, Fscale_features, new_sf_den_all, DFT_ROWS, true); - reduce(new_sf_den_all, new_sf_den, 0, CV_REDUCE_SUM, -1); + reduce(new_sf_den_all, new_sf_den, 0, REDUCE_SUM, -1); sf_num = (1 - learn_rate) * sf_num + learn_rate * new_sf_num; sf_den = (1 - learn_rate) * sf_den + learn_rate * new_sf_den; @@ -194,7 +194,7 @@ float DSST::getScale(const Mat &image, const Point2f object_center) mulSpectrums(Fscale_features, sf_num, Fscale_features, 0, false); Mat scale_resp; - reduce(Fscale_features, scale_resp, 0, CV_REDUCE_SUM, -1); + reduce(Fscale_features, scale_resp, 0, REDUCE_SUM, -1); scale_resp = divide_complex_matrices(scale_resp, sf_den + 0.01f); idft(scale_resp, scale_resp, DFT_REAL_OUTPUT|DFT_SCALE); Point max_loc; diff --git a/modules/tracking/src/trackerFeature.cpp b/modules/tracking/src/trackerFeature.cpp index a5b59bdcfa1..3f86fb5d0b8 100644 --- a/modules/tracking/src/trackerFeature.cpp +++ b/modules/tracking/src/trackerFeature.cpp @@ -41,8 +41,9 @@ #include "precomp.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { /* * TrackerFeature @@ -321,4 +322,4 @@ void TrackerFeatureLBP::selection( Mat& /*response*/, int /*npoints*/) } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/trackerFeatureSet.cpp b/modules/tracking/src/trackerFeatureSet.cpp index 9896f0ffa29..dfa847a5123 100644 --- a/modules/tracking/src/trackerFeatureSet.cpp +++ b/modules/tracking/src/trackerFeatureSet.cpp @@ -41,8 +41,9 @@ #include "precomp.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { /* * TrackerFeatureSet @@ -139,4 +140,5 @@ void TrackerFeatureSet::clearResponses() responses.clear(); } -} /* namespace cv */ + +}}} // namespace diff --git a/modules/tracking/src/trackerKCF.cpp b/modules/tracking/src/trackerKCF.cpp new file mode 100644 index 00000000000..9623f2f1b08 --- /dev/null +++ b/modules/tracking/src/trackerKCF.cpp @@ -0,0 +1,938 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// + // + // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. + // + // By downloading, copying, installing or using the software you agree to this license. + // If you do not agree to this license, do not download, install, + // copy or use the software. + // + // + // License Agreement + // For Open Source Computer Vision Library + // + // Copyright (C) 2013, OpenCV Foundation, all rights reserved. + // Third party copyrights are property of their respective owners. + // + // Redistribution and use in source and binary forms, with or without modification, + // are permitted provided that the following conditions are met: + // + // * Redistribution's of source code must retain the above copyright notice, + // this list of conditions and the following disclaimer. + // + // * Redistribution's in binary form must reproduce the above copyright notice, + // this list of conditions and the following disclaimer in the documentation + // and/or other materials provided with the distribution. + // + // * The name of the copyright holders may not be used to endorse or promote products + // derived from this software without specific prior written permission. + // + // This software is provided by the copyright holders and contributors "as is" and + // any express or implied warranties, including, but not limited to, the implied + // warranties of merchantability and fitness for a particular purpose are disclaimed. + // In no event shall the Intel Corporation or contributors be liable for any direct, + // indirect, incidental, special, exemplary, or consequential damages + // (including, but not limited to, procurement of substitute goods or services; + // loss of use, data, or profits; or business interruption) however caused + // and on any theory of liability, whether in contract, strict liability, + // or tort (including negligence or otherwise) arising in any way out of + // the use of this software, even if advised of the possibility of such damage. + // + //M*/ + +#include "precomp.hpp" + +#include "opencl_kernels_tracking.hpp" +#include +#include + +namespace cv { +inline namespace tracking { +namespace impl { + +/*--------------------------- +| TrackerKCFModel +|---------------------------*/ + /** + * \brief Implementation of TrackerModel for KCF algorithm + */ + class TrackerKCFModel : public TrackerModel{ + public: + TrackerKCFModel(){} + ~TrackerKCFModel(){} + protected: + void modelEstimationImpl( const std::vector& /*responses*/ ) CV_OVERRIDE {} + void modelUpdateImpl() CV_OVERRIDE {} + }; + + +/*--------------------------- +| TrackerKCF +|---------------------------*/ + /* + * Prototype + */ +class TrackerKCFImpl CV_FINAL : public TrackerKCF +{ +public: + TrackerKCFImpl(const TrackerKCF::Params ¶meters); + + virtual void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + virtual bool update(InputArray image, Rect& boundingBox) CV_OVERRIDE; + void setFeatureExtractor(void (*f)(const Mat, const Rect, Mat&), bool pca_func = false) CV_OVERRIDE; + + TrackerKCF::Params params; + Ptr model; + +protected: + void createHanningWindow(OutputArray dest, const cv::Size winSize, const int type) const; + void inline fft2(const Mat src, std::vector & dest, std::vector & layers_data) const; + void inline fft2(const Mat src, Mat & dest) const; + void inline ifft2(const Mat src, Mat & dest) const; + void inline pixelWiseMult(const std::vector src1, const std::vector src2, std::vector & dest, const int flags, const bool conjB=false) const; + void inline sumChannels(std::vector src, Mat & dest) const; + void inline updateProjectionMatrix(const Mat src, Mat & old_cov,Mat & proj_matrix,float pca_rate, int compressed_sz, + std::vector & layers_pca,std::vector & average, Mat pca_data, Mat new_cov, Mat w, Mat u, Mat v); + void inline compress(const Mat proj_matrix, const Mat src, Mat & dest, Mat & data, Mat & compressed) const; + bool getSubWindow(const Mat img, const Rect roi, Mat& feat, Mat& patch, TrackerKCF::MODE desc = GRAY) const; + bool getSubWindow(const Mat img, const Rect roi, Mat& feat, void (*f)(const Mat, const Rect, Mat& )) const; + void extractCN(Mat patch_data, Mat & cnFeatures) const; + void denseGaussKernel(const float sigma, const Mat , const Mat y_data, Mat & k_data, + std::vector & layers_data,std::vector & xf_data,std::vector & yf_data, std::vector xyf_v, Mat xy, Mat xyf ) const; + void calcResponse(const Mat alphaf_data, const Mat kf_data, Mat & response_data, Mat & spec_data) const; + void calcResponse(const Mat alphaf_data, const Mat alphaf_den_data, const Mat kf_data, Mat & response_data, Mat & spec_data, Mat & spec2_data) const; + + void shiftRows(Mat& mat) const; + void shiftRows(Mat& mat, int n) const; + void shiftCols(Mat& mat, int n) const; +#ifdef HAVE_OPENCL + bool inline oclTransposeMM(const Mat src, float alpha, UMat &dst); +#endif + +private: + float output_sigma; + Rect2d roi; + Mat hann; //hann window filter + Mat hann_cn; //10 dimensional hann-window filter for CN features, + + Mat y,yf; // training response and its FFT + Mat x; // observation and its FFT + Mat k,kf; // dense gaussian kernel and its FFT + Mat kf_lambda; // kf+lambda + Mat new_alphaf, alphaf; // training coefficients + Mat new_alphaf_den, alphaf_den; // for splitted training coefficients + Mat z; // model + Mat response; // detection result + Mat old_cov_mtx, proj_mtx; // for feature compression + + // pre-defined Mat variables for optimization of private functions + Mat spec, spec2; + std::vector layers; + std::vector vxf,vyf,vxyf; + Mat xy_data,xyf_data; + Mat data_temp, compress_data; + std::vector layers_pca_data; + std::vector average_data; + Mat img_Patch; + + // storage for the extracted features, KRLS model, KRLS compressed model + Mat X[2],Z[2],Zc[2]; + + // storage of the extracted features + std::vector features_pca; + std::vector features_npca; + std::vector descriptors_pca; + std::vector descriptors_npca; + + // optimization variables for updateProjectionMatrix + Mat data_pca, new_covar,w_data,u_data,vt_data; + + // custom feature extractor + bool use_custom_extractor_pca; + bool use_custom_extractor_npca; + std::vector extractor_pca; + std::vector extractor_npca; + + bool resizeImage; // resize the image whenever needed and the patch size is large + +#ifdef HAVE_OPENCL + ocl::Kernel transpose_mm_ker; // OCL kernel to compute transpose matrix multiply matrix. +#endif + + int frame; +}; + + /* + * Constructor + */ + TrackerKCFImpl::TrackerKCFImpl( const TrackerKCF::Params ¶meters ) : + params( parameters ) + { + resizeImage = false; + use_custom_extractor_pca = false; + use_custom_extractor_npca = false; + +#ifdef HAVE_OPENCL + // For update proj matrix's multiplication + if(ocl::useOpenCL()) + { + cv::String err; + ocl::ProgramSource tmmSrc = ocl::tracking::tmm_oclsrc; + ocl::Program tmmProg(tmmSrc, String(), err); + transpose_mm_ker.create("tmm", tmmProg); + } +#endif + } + + /* + * Initialization: + * - creating hann window filter + * - ROI padding + * - creating a gaussian response for the training ground-truth + * - perform FFT to the gaussian response + */ + void TrackerKCFImpl::init(InputArray image, const Rect& boundingBox) + { + frame=0; + roi.x = cvRound(boundingBox.x); + roi.y = cvRound(boundingBox.y); + roi.width = cvRound(boundingBox.width); + roi.height = cvRound(boundingBox.height); + + //calclulate output sigma + output_sigma=std::sqrt(static_cast(roi.width*roi.height))*params.output_sigma_factor; + output_sigma=-0.5f/(output_sigma*output_sigma); + + //resize the ROI whenever needed + if(params.resize && roi.width*roi.height>params.max_patch_size){ + resizeImage=true; + roi.x/=2.0; + roi.y/=2.0; + roi.width/=2.0; + roi.height/=2.0; + } + + // add padding to the roi + roi.x-=roi.width/2; + roi.y-=roi.height/2; + roi.width*=2; + roi.height*=2; + + // initialize the hann window filter + createHanningWindow(hann, roi.size(), CV_32F); + + // hann window filter for CN feature + Mat _layer[] = {hann, hann, hann, hann, hann, hann, hann, hann, hann, hann}; + merge(_layer, 10, hann_cn); + + // create gaussian response + y=Mat::zeros((int)roi.height,(int)roi.width,CV_32F); + for(int i=0;i(i,j) = + static_cast((i-roi.height/2+1)*(i-roi.height/2+1)+(j-roi.width/2+1)*(j-roi.width/2+1)); + } + } + + y*=(float)output_sigma; + cv::exp(y,y); + + // perform fourier transfor to the gaussian response + fft2(y,yf); + + if (image.channels() == 1) { // disable CN for grayscale images + params.desc_pca &= ~(CN); + params.desc_npca &= ~(CN); + } + model = makePtr(); + + // record the non-compressed descriptors + if((params.desc_npca & GRAY) == GRAY)descriptors_npca.push_back(GRAY); + if((params.desc_npca & CN) == CN)descriptors_npca.push_back(CN); + if(use_custom_extractor_npca)descriptors_npca.push_back(CUSTOM); + features_npca.resize(descriptors_npca.size()); + + // record the compressed descriptors + if((params.desc_pca & GRAY) == GRAY)descriptors_pca.push_back(GRAY); + if((params.desc_pca & CN) == CN)descriptors_pca.push_back(CN); + if(use_custom_extractor_pca)descriptors_pca.push_back(CUSTOM); + features_pca.resize(descriptors_pca.size()); + + // accept only the available descriptor modes + CV_Assert( + (params.desc_pca & GRAY) == GRAY + || (params.desc_npca & GRAY) == GRAY + || (params.desc_pca & CN) == CN + || (params.desc_npca & CN) == CN + || use_custom_extractor_pca + || use_custom_extractor_npca + ); + + // ensure roi has intersection with the image + Rect2d image_roi(0, 0, + image.cols() / (resizeImage ? 2 : 1), + image.rows() / (resizeImage ? 2 : 1)); + CV_Assert(!(roi & image_roi).empty()); + } + + /* + * Main part of the KCF algorithm + */ + bool TrackerKCFImpl::update(InputArray image, Rect& boundingBoxResult) + { + double minVal, maxVal; // min-max response + Point minLoc,maxLoc; // min-max location + + CV_Assert(image.channels() == 1 || image.channels() == 3); + + Mat img; + // resize the image whenever needed + if (resizeImage) + resize(image, img, Size(image.cols()/2, image.rows()/2), 0, 0, INTER_LINEAR_EXACT); + else + image.copyTo(img); + + // detection part + if(frame>0){ + + // extract and pre-process the patch + // get non compressed descriptors + for(unsigned i=0;i0)merge(features_npca,X[1]); + + // get compressed descriptors + for(unsigned i=0;i0)merge(features_pca,X[0]); + + //compress the features and the KRSL model + if(params.desc_pca !=0){ + compress(proj_mtx,X[0],X[0],data_temp,compress_data); + compress(proj_mtx,Z[0],Zc[0],data_temp,compress_data); + } + + // copy the compressed KRLS model + Zc[1] = Z[1]; + + // merge all features + if(features_npca.size()==0){ + x = X[0]; + z = Zc[0]; + }else if(features_pca.size()==0){ + x = X[1]; + z = Z[1]; + }else{ + merge(X,2,x); + merge(Zc,2,z); + } + + //compute the gaussian kernel + denseGaussKernel(params.sigma,x,z,k,layers,vxf,vyf,vxyf,xy_data,xyf_data); + + // compute the fourier transform of the kernel + fft2(k,kf); + if(frame==1)spec2=Mat_(kf.rows, kf.cols); + + // calculate filter response + if(params.split_coeff) + calcResponse(alphaf,alphaf_den,kf,response, spec, spec2); + else + calcResponse(alphaf,kf,response, spec); + + // extract the maximum response + minMaxLoc( response, &minVal, &maxVal, &minLoc, &maxLoc ); + if (maxVal < params.detect_thresh) + { + return false; + } + roi.x+=(maxLoc.x-roi.width/2+1); + roi.y+=(maxLoc.y-roi.height/2+1); + } + + // update the bounding box + Rect2d boundingBox; + boundingBox.x=(resizeImage?roi.x*2:roi.x)+(resizeImage?roi.width*2:roi.width)/4; + boundingBox.y=(resizeImage?roi.y*2:roi.y)+(resizeImage?roi.height*2:roi.height)/4; + boundingBox.width = (resizeImage?roi.width*2:roi.width)/2; + boundingBox.height = (resizeImage?roi.height*2:roi.height)/2; + + // extract the patch for learning purpose + // get non compressed descriptors + for(unsigned i=0;i0)merge(features_npca,X[1]); + + // get compressed descriptors + for(unsigned i=0;i0)merge(features_pca,X[0]); + + //update the training data + if(frame==0){ + Z[0] = X[0].clone(); + Z[1] = X[1].clone(); + }else{ + Z[0]=(1.0-params.interp_factor)*Z[0]+params.interp_factor*X[0]; + Z[1]=(1.0-params.interp_factor)*Z[1]+params.interp_factor*X[1]; + } + + if(params.desc_pca !=0 || use_custom_extractor_pca){ + // initialize the vector of Mat variables + if(frame==0){ + layers_pca_data.resize(Z[0].channels()); + average_data.resize(Z[0].channels()); + } + + // feature compression + updateProjectionMatrix(Z[0],old_cov_mtx,proj_mtx,params.pca_learning_rate,params.compressed_size,layers_pca_data,average_data,data_pca, new_covar,w_data,u_data,vt_data); + compress(proj_mtx,X[0],X[0],data_temp,compress_data); + } + + // merge all features + if(features_npca.size()==0) + x = X[0]; + else if(features_pca.size()==0) + x = X[1]; + else + merge(X,2,x); + + // initialize some required Mat variables + if(frame==0){ + layers.resize(x.channels()); + vxf.resize(x.channels()); + vyf.resize(x.channels()); + vxyf.resize(vyf.size()); + new_alphaf=Mat_(yf.rows, yf.cols); + } + + // Kernel Regularized Least-Squares, calculate alphas + denseGaussKernel(params.sigma,x,x,k,layers,vxf,vyf,vxyf,xy_data,xyf_data); + + // compute the fourier transform of the kernel and add a small value + fft2(k,kf); + kf_lambda=kf+params.lambda; + + float den; + if(params.split_coeff){ + mulSpectrums(yf,kf,new_alphaf,0); + mulSpectrums(kf,kf_lambda,new_alphaf_den,0); + }else{ + for(int i=0;i(i,j)[0]*kf_lambda.at(i,j)[0]+kf_lambda.at(i,j)[1]*kf_lambda.at(i,j)[1]); + + new_alphaf.at(i,j)[0]= + (yf.at(i,j)[0]*kf_lambda.at(i,j)[0]+yf.at(i,j)[1]*kf_lambda.at(i,j)[1])*den; + new_alphaf.at(i,j)[1]= + (yf.at(i,j)[1]*kf_lambda.at(i,j)[0]-yf.at(i,j)[0]*kf_lambda.at(i,j)[1])*den; + } + } + } + + // update the RLS model + if(frame==0){ + alphaf=new_alphaf.clone(); + if(params.split_coeff)alphaf_den=new_alphaf_den.clone(); + }else{ + alphaf=(1.0-params.interp_factor)*alphaf+params.interp_factor*new_alphaf; + if(params.split_coeff)alphaf_den=(1.0-params.interp_factor)*alphaf_den+params.interp_factor*new_alphaf_den; + } + + frame++; + + int x1 = cvRound(boundingBox.x); + int y1 = cvRound(boundingBox.y); + int x2 = cvRound(boundingBox.x + boundingBox.width); + int y2 = cvRound(boundingBox.y + boundingBox.height); + boundingBoxResult = Rect(x1, y1, x2 - x1, y2 - y1) & Rect(Point(0, 0), image.size()); + + return true; + } + + + /*------------------------------------- + | implementation of the KCF functions + |-------------------------------------*/ + + /* + * hann window filter + */ + void TrackerKCFImpl::createHanningWindow(OutputArray dest, const cv::Size winSize, const int type) const { + CV_Assert( type == CV_32FC1 || type == CV_64FC1 ); + + dest.create(winSize, type); + Mat dst = dest.getMat(); + + int rows = dst.rows, cols = dst.cols; + + AutoBuffer _wc(cols); + float * const wc = _wc.data(); + + const float coeff0 = 2.0f * (float)CV_PI / (cols - 1); + const float coeff1 = 2.0f * (float)CV_PI / (rows - 1); + for(int j = 0; j < cols; j++) + wc[j] = 0.5f * (1.0f - cos(coeff0 * j)); + + if(dst.depth() == CV_32F){ + for(int i = 0; i < rows; i++){ + float* dstData = dst.ptr(i); + float wr = 0.5f * (1.0f - cos(coeff1 * i)); + for(int j = 0; j < cols; j++) + dstData[j] = (float)(wr * wc[j]); + } + }else{ + for(int i = 0; i < rows; i++){ + double* dstData = dst.ptr(i); + double wr = 0.5f * (1.0f - cos(coeff1 * i)); + for(int j = 0; j < cols; j++) + dstData[j] = wr * wc[j]; + } + } + + // perform batch sqrt for SSE performance gains + //cv::sqrt(dst, dst); //matlab do not use the square rooted version + } + + /* + * simplification of fourier transform function in opencv + */ + void inline TrackerKCFImpl::fft2(const Mat src, Mat & dest) const { + dft(src,dest,DFT_COMPLEX_OUTPUT); + } + + void inline TrackerKCFImpl::fft2(const Mat src, std::vector & dest, std::vector & layers_data) const { + split(src, layers_data); + + for(int i=0;i src1, const std::vector src2, std::vector & dest, const int flags, const bool conjB) const { + for(unsigned i=0;i src, Mat & dest) const { + dest=src[0].clone(); + for(unsigned i=1;i(src.cols * 64), static_cast(src.cols)}; + size_t localSize[2] = {64, 1}; + if (!transpose_mm_ker.run(2, globSize, localSize, true)) + return false; + return true; + } +#endif + + /* + * obtains the projection matrix using PCA + */ + void inline TrackerKCFImpl::updateProjectionMatrix(const Mat src, Mat & old_cov,Mat & proj_matrix, float pca_rate, int compressed_sz, + std::vector & layers_pca,std::vector & average, Mat pca_data, Mat new_cov, Mat w, Mat u, Mat vt) { + CV_Assert(compressed_sz<=src.channels()); + + split(src,layers_pca); + + for (int i=0;i(i, j) - result.getMat(ACCESS_RW).at(i , j)) > abs(new_cov.at(i, j)) * 1e-3) + printf("error @ i %d j %d got %G expected %G \n", i, j, result.getMat(ACCESS_RW).at(i , j), new_cov.at(i, j)); +#endif + if(old_cov.rows==0)old_cov=new_cov.clone(); + SVD::compute((1.0f - pca_rate) * old_cov + pca_rate * new_cov, w, u, vt); + } +#else + new_cov=1.0/(float)(src.rows*src.cols-1)*(pca_data.t()*pca_data); + if(old_cov.rows==0)old_cov=new_cov.clone(); + + // calc PCA + SVD::compute((1.0-pca_rate)*old_cov+pca_rate*new_cov, w, u, vt); +#endif + // extract the projection matrix + proj_matrix=u(Rect(0,0,compressed_sz,src.channels())).clone(); + Mat proj_vars=Mat::eye(compressed_sz,compressed_sz,proj_matrix.type()); + for(int i=0;i(i,i)=w.at(i); + } + + // update the covariance matrix + old_cov=(1.0-pca_rate)*old_cov+pca_rate*proj_matrix*proj_vars*proj_matrix.t(); + } + + /* + * compress the features + */ + void inline TrackerKCFImpl::compress(const Mat proj_matrix, const Mat src, Mat & dest, Mat & data, Mat & compressed) const { + data=src.reshape(1,src.rows*src.cols); + compressed=data*proj_matrix; + dest=compressed.reshape(proj_matrix.cols,src.rows).clone(); + } + + /* + * obtain the patch and apply hann window filter to it + */ + bool TrackerKCFImpl::getSubWindow(const Mat img, const Rect _roi, Mat& feat, Mat& patch, TrackerKCF::MODE desc) const { + + Rect region=_roi; + + // return false if roi is outside the image + if ((roi & Rect2d(0, 0, img.cols, img.rows)).empty()) + return false; + + // extract patch inside the image + if(_roi.x<0){region.x=0;region.width+=_roi.x;} + if(_roi.y<0){region.y=0;region.height+=_roi.y;} + if(_roi.x+_roi.width>img.cols)region.width=img.cols-_roi.x; + if(_roi.y+_roi.height>img.rows)region.height=img.rows-_roi.y; + if(region.width>img.cols)region.width=img.cols; + if(region.height>img.rows)region.height=img.rows; + + // return false if region is empty + if (region.empty()) + return false; + + patch=img(region).clone(); + + // add some padding to compensate when the patch is outside image border + int addTop,addBottom, addLeft, addRight; + addTop=region.y-_roi.y; + addBottom=(_roi.height+_roi.y>img.rows?_roi.height+_roi.y-img.rows:0); + addLeft=region.x-_roi.x; + addRight=(_roi.width+_roi.x>img.cols?_roi.width+_roi.x-img.cols:0); + + copyMakeBorder(patch,patch,addTop,addBottom,addLeft,addRight,BORDER_REPLICATE); + if(patch.rows==0 || patch.cols==0)return false; + + // extract the desired descriptors + switch(desc){ + case CN: + CV_Assert(img.channels() == 3); + extractCN(patch,feat); + feat=feat.mul(hann_cn); // hann window filter + break; + default: // GRAY + if(img.channels()>1) + cvtColor(patch,feat, COLOR_BGR2GRAY); + else + feat=patch; + //feat.convertTo(feat,CV_32F); + feat.convertTo(feat,CV_32F, 1.0/255.0, -0.5); + //feat=feat/255.0-0.5; // normalize to range -0.5 .. 0.5 + feat=feat.mul(hann); // hann window filter + break; + } + + return true; + + } + + /* + * get feature using external function + */ + bool TrackerKCFImpl::getSubWindow(const Mat img, const Rect _roi, Mat& feat, void (*f)(const Mat, const Rect, Mat& )) const{ + + // return false if roi is outside the image + if((_roi.x+_roi.width<0) + ||(_roi.y+_roi.height<0) + ||(_roi.x>=img.cols) + ||(_roi.y>=img.rows) + )return false; + + f(img, _roi, feat); + + if(_roi.width != feat.cols || _roi.height != feat.rows){ + printf("error in customized function of features extractor!\n"); + printf("Rules: roi.width==feat.cols && roi.height = feat.rows \n"); + } + + Mat hann_win; + std::vector _layers; + + for(int i=0;i(0,0); + unsigned index; + + if(cnFeatures.type() != CV_32FC(10)) + cnFeatures = Mat::zeros(patch_data.rows,patch_data.cols,CV_32FC(10)); + + for(int i=0;i(i,j); + index=(unsigned)(floor((float)pixel[2]/8)+32*floor((float)pixel[1]/8)+32*32*floor((float)pixel[0]/8)); + + //copy the values + for(int _k=0;_k<10;_k++){ + cnFeatures.at >(i,j)[_k]=ColorNames[index][_k]; + } + } + } + + } + + /* + * dense gauss kernel function + */ + void TrackerKCFImpl::denseGaussKernel(const float sigma, const Mat x_data, const Mat y_data, Mat & k_data, + std::vector & layers_data,std::vector & xf_data,std::vector & yf_data, std::vector xyf_v, Mat xy, Mat xyf ) const { + double normX, normY; + + fft2(x_data,xf_data,layers_data); + fft2(y_data,yf_data,layers_data); + + normX=norm(x_data); + normX*=normX; + normY=norm(y_data); + normY*=normY; + + pixelWiseMult(xf_data,yf_data,xyf_v,0,true); + sumChannels(xyf_v,xyf); + ifft2(xyf,xyf); + + if(params.wrap_kernel){ + shiftRows(xyf, x_data.rows/2); + shiftCols(xyf, x_data.cols/2); + } + + //(xx + yy - 2 * xy) / numel(x) + xy=(normX+normY-2*xyf)/(x_data.rows*x_data.cols*x_data.channels()); + + // TODO: check wether we really need thresholding or not + //threshold(xy,xy,0.0,0.0,THRESH_TOZERO);//max(0, (xx + yy - 2 * xy) / numel(x)) + for(int i=0;i(i,j)<0.0)xy.at(i,j)=0.0; + } + } + + float sig=-1.0f/(sigma*sigma); + xy=sig*xy; + exp(xy,k_data); + + } + + /* CIRCULAR SHIFT Function + * http://stackoverflow.com/questions/10420454/shift-like-matlab-function-rows-or-columns-of-a-matrix-in-opencv + */ + // circular shift one row from up to down + void TrackerKCFImpl::shiftRows(Mat& mat) const { + + Mat temp; + Mat m; + int _k = (mat.rows-1); + mat.row(_k).copyTo(temp); + for(; _k > 0 ; _k-- ) { + m = mat.row(_k); + mat.row(_k-1).copyTo(m); + } + m = mat.row(0); + temp.copyTo(m); + + } + + // circular shift n rows from up to down if n > 0, -n rows from down to up if n < 0 + void TrackerKCFImpl::shiftRows(Mat& mat, int n) const { + if( n < 0 ) { + n = -n; + flip(mat,mat,0); + for(int _k=0; _k < n;_k++) { + shiftRows(mat); + } + flip(mat,mat,0); + }else{ + for(int _k=0; _k < n;_k++) { + shiftRows(mat); + } + } + } + + //circular shift n columns from left to right if n > 0, -n columns from right to left if n < 0 + void TrackerKCFImpl::shiftCols(Mat& mat, int n) const { + if(n < 0){ + n = -n; + flip(mat,mat,1); + transpose(mat,mat); + shiftRows(mat,n); + transpose(mat,mat); + flip(mat,mat,1); + }else{ + transpose(mat,mat); + shiftRows(mat,n); + transpose(mat,mat); + } + } + + /* + * calculate the detection response + */ + void TrackerKCFImpl::calcResponse(const Mat alphaf_data, const Mat kf_data, Mat & response_data, Mat & spec_data) const { + //alpha f--> 2channels ; k --> 1 channel; + mulSpectrums(alphaf_data,kf_data,spec_data,0,false); + ifft2(spec_data,response_data); + } + + /* + * calculate the detection response for splitted form + */ + void TrackerKCFImpl::calcResponse(const Mat alphaf_data, const Mat _alphaf_den, const Mat kf_data, Mat & response_data, Mat & spec_data, Mat & spec2_data) const { + + mulSpectrums(alphaf_data,kf_data,spec_data,0,false); + + //z=(a+bi)/(c+di)=[(ac+bd)+i(bc-ad)]/(c^2+d^2) + float den; + for(int i=0;i(i,j)[0]*_alphaf_den.at(i,j)[0]+_alphaf_den.at(i,j)[1]*_alphaf_den.at(i,j)[1]); + spec2_data.at(i,j)[0]= + (spec_data.at(i,j)[0]*_alphaf_den.at(i,j)[0]+spec_data.at(i,j)[1]*_alphaf_den.at(i,j)[1])*den; + spec2_data.at(i,j)[1]= + (spec_data.at(i,j)[1]*_alphaf_den.at(i,j)[0]-spec_data.at(i,j)[0]*_alphaf_den.at(i,j)[1])*den; + } + } + + ifft2(spec2_data,response_data); + } + + void TrackerKCFImpl::setFeatureExtractor(void (*f)(const Mat, const Rect, Mat&), bool pca_func){ + if(pca_func){ + extractor_pca.push_back(f); + use_custom_extractor_pca = true; + }else{ + extractor_npca.push_back(f); + use_custom_extractor_npca = true; + } + } + /*----------------------------------------------------------------------*/ + + +} // namespace + +TrackerKCF::Params::Params() +{ + detect_thresh = 0.5f; + sigma=0.2f; + lambda=0.0001f; + interp_factor=0.075f; + output_sigma_factor=1.0f / 16.0f; + resize=true; + max_patch_size=80*80; + split_coeff=true; + wrap_kernel=false; + desc_npca = GRAY; + desc_pca = CN; + + //feature compression + compress_feature=true; + compressed_size=2; + pca_learning_rate=0.15f; +} + + +TrackerKCF::TrackerKCF() +{ + // nothing +} + +TrackerKCF::~TrackerKCF() +{ + // nothing +} + +Ptr TrackerKCF::create(const TrackerKCF::Params ¶meters) +{ + return makePtr(parameters); +} + +}} // namespace + +#include "legacy/trackerKCF.legacy.hpp" diff --git a/modules/tracking/src/trackerMIL.cpp b/modules/tracking/src/trackerMIL.cpp new file mode 100644 index 00000000000..3d3c22c1f73 --- /dev/null +++ b/modules/tracking/src/trackerMIL.cpp @@ -0,0 +1,265 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// + // + // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. + // + // By downloading, copying, installing or using the software you agree to this license. + // If you do not agree to this license, do not download, install, + // copy or use the software. + // + // + // License Agreement + // For Open Source Computer Vision Library + // + // Copyright (C) 2013, OpenCV Foundation, all rights reserved. + // Third party copyrights are property of their respective owners. + // + // Redistribution and use in source and binary forms, with or without modification, + // are permitted provided that the following conditions are met: + // + // * Redistribution's of source code must retain the above copyright notice, + // this list of conditions and the following disclaimer. + // + // * Redistribution's in binary form must reproduce the above copyright notice, + // this list of conditions and the following disclaimer in the documentation + // and/or other materials provided with the distribution. + // + // * The name of the copyright holders may not be used to endorse or promote products + // derived from this software without specific prior written permission. + // + // This software is provided by the copyright holders and contributors "as is" and + // any express or implied warranties, including, but not limited to, the implied + // warranties of merchantability and fitness for a particular purpose are disclaimed. + // In no event shall the Intel Corporation or contributors be liable for any direct, + // indirect, incidental, special, exemplary, or consequential damages + // (including, but not limited to, procurement of substitute goods or services; + // loss of use, data, or profits; or business interruption) however caused + // and on any theory of liability, whether in contract, strict liability, + // or tort (including negligence or otherwise) arising in any way out of + // the use of this software, even if advised of the possibility of such damage. + // + //M*/ + +#include "precomp.hpp" +#include "trackerMILModel.hpp" + +namespace cv { +inline namespace tracking { +namespace impl { + +class TrackerMILImpl CV_FINAL : public TrackerMIL +{ +public: + TrackerMILImpl(const TrackerMIL::Params ¶meters); + + virtual void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; + virtual bool update(InputArray image, Rect& boundingBox) CV_OVERRIDE; + + + void compute_integral( const Mat & img, Mat & ii_img ); + + TrackerMIL::Params params; + + Ptr model; + Ptr sampler; + Ptr featureSet; +}; + +} // namespace + +TrackerMIL::Params::Params() +{ + samplerInitInRadius = 3; + samplerSearchWinSize = 25; + samplerInitMaxNegNum = 65; + samplerTrackInRadius = 4; + samplerTrackMaxPosNum = 100000; + samplerTrackMaxNegNum = 65; + featureSetNumFeatures = 250; +} + +namespace impl { + +TrackerMILImpl::TrackerMILImpl(const TrackerMIL::Params ¶meters) + : params(parameters) +{ + // nothing +} + +void TrackerMILImpl::compute_integral( const Mat & img, Mat & ii_img ) +{ + Mat ii; + std::vector ii_imgs; + integral( img, ii, CV_32F ); // FIXIT split first + split( ii, ii_imgs ); + ii_img = ii_imgs[0]; +} + +void TrackerMILImpl::init(InputArray image, const Rect& boundingBox) +{ + sampler = makePtr(); + featureSet = makePtr(); + + Mat intImage; + compute_integral(image.getMat(), intImage); + TrackerSamplerCSC::Params CSCparameters; + CSCparameters.initInRad = params.samplerInitInRadius; + CSCparameters.searchWinSize = params.samplerSearchWinSize; + CSCparameters.initMaxNegNum = params.samplerInitMaxNegNum; + CSCparameters.trackInPosRad = params.samplerTrackInRadius; + CSCparameters.trackMaxPosNum = params.samplerTrackMaxPosNum; + CSCparameters.trackMaxNegNum = params.samplerTrackMaxNegNum; + + Ptr CSCSampler = makePtr(CSCparameters); + CV_Assert(sampler->addTrackerSamplerAlgorithm(CSCSampler)); + + //or add CSC sampler with default parameters + //sampler->addTrackerSamplerAlgorithm( "CSC" ); + + //Positive sampling + CSCSampler.staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_POS ); + sampler->sampling( intImage, boundingBox ); + std::vector posSamples = sampler->getSamples(); + + //Negative sampling + CSCSampler.staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_NEG ); + sampler->sampling( intImage, boundingBox ); + std::vector negSamples = sampler->getSamples(); + + CV_Assert(!posSamples.empty()); + CV_Assert(!negSamples.empty()); + + //compute HAAR features + TrackerFeatureHAAR::Params HAARparameters; + HAARparameters.numFeatures = params.featureSetNumFeatures; + HAARparameters.rectSize = Size( (int)boundingBox.width, (int)boundingBox.height ); + HAARparameters.isIntegral = true; + Ptr trackerFeature = Ptr( new TrackerFeatureHAAR( HAARparameters ) ); + featureSet->addTrackerFeature( trackerFeature ); + + featureSet->extraction( posSamples ); + const std::vector posResponse = featureSet->getResponses(); + + featureSet->extraction( negSamples ); + const std::vector negResponse = featureSet->getResponses(); + + model = makePtr(boundingBox); + Ptr stateEstimator = Ptr( + new TrackerStateEstimatorMILBoosting( params.featureSetNumFeatures ) ); + model->setTrackerStateEstimator( stateEstimator ); + + //Run model estimation and update + model.staticCast()->setMode( TrackerMILModel::MODE_POSITIVE, posSamples ); + model->modelEstimation( posResponse ); + model.staticCast()->setMode( TrackerMILModel::MODE_NEGATIVE, negSamples ); + model->modelEstimation( negResponse ); + model->modelUpdate(); +} + +bool TrackerMILImpl::update(InputArray image, Rect& boundingBox) +{ + Mat intImage; + compute_integral(image.getMat(), intImage); + + //get the last location [AAM] X(k-1) + Ptr lastLocation = model->getLastTargetState(); + Rect lastBoundingBox( (int)lastLocation->getTargetPosition().x, (int)lastLocation->getTargetPosition().y, lastLocation->getTargetWidth(), + lastLocation->getTargetHeight() ); + + //sampling new frame based on last location + ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_DETECT ); + sampler->sampling( intImage, lastBoundingBox ); + std::vector detectSamples = sampler->getSamples(); + if( detectSamples.empty() ) + return false; + + /*//TODO debug samples + Mat f; + image.copyTo(f); + + for( size_t i = 0; i < detectSamples.size(); i=i+10 ) + { + Size sz; + Point off; + detectSamples.at(i).locateROI(sz, off); + rectangle(f, Rect(off.x,off.y,detectSamples.at(i).cols,detectSamples.at(i).rows), Scalar(255,0,0), 1); + }*/ + + //extract features from new samples + featureSet->extraction( detectSamples ); + std::vector response = featureSet->getResponses(); + + //predict new location + ConfidenceMap cmap; + model.staticCast()->setMode( TrackerMILModel::MODE_ESTIMATON, detectSamples ); + model.staticCast()->responseToConfidenceMap( response, cmap ); + model->getTrackerStateEstimator().staticCast()->setCurrentConfidenceMap( cmap ); + + if( !model->runStateEstimator() ) + { + return false; + } + + Ptr currentState = model->getLastTargetState(); + boundingBox = Rect( (int)currentState->getTargetPosition().x, (int)currentState->getTargetPosition().y, currentState->getTargetWidth(), + currentState->getTargetHeight() ); + + /*//TODO debug + rectangle(f, lastBoundingBox, Scalar(0,255,0), 1); + rectangle(f, boundingBox, Scalar(0,0,255), 1); + imshow("f", f); + //waitKey( 0 );*/ + + //sampling new frame based on new location + //Positive sampling + ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_POS ); + sampler->sampling( intImage, boundingBox ); + std::vector posSamples = sampler->getSamples(); + + //Negative sampling + ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_NEG ); + sampler->sampling( intImage, boundingBox ); + std::vector negSamples = sampler->getSamples(); + + if( posSamples.empty() || negSamples.empty() ) + return false; + + //extract features + featureSet->extraction( posSamples ); + std::vector posResponse = featureSet->getResponses(); + + featureSet->extraction( negSamples ); + std::vector negResponse = featureSet->getResponses(); + + //model estimate + model.staticCast()->setMode( TrackerMILModel::MODE_POSITIVE, posSamples ); + model->modelEstimation( posResponse ); + model.staticCast()->setMode( TrackerMILModel::MODE_NEGATIVE, negSamples ); + model->modelEstimation( negResponse ); + + //model update + model->modelUpdate(); + + return true; +} + +} // namespace + + +TrackerMIL::TrackerMIL() +{ + // nothing +} + +TrackerMIL::~TrackerMIL() +{ + // nothing +} + +Ptr TrackerMIL::create(const TrackerMIL::Params ¶meters) +{ + return makePtr(parameters); +} + +}} // namespace + +#include "legacy/trackerMIL.legacy.hpp" diff --git a/modules/tracking/src/trackerMILModel.cpp b/modules/tracking/src/trackerMILModel.cpp index f6abec68f83..0d792890470 100644 --- a/modules/tracking/src/trackerMILModel.cpp +++ b/modules/tracking/src/trackerMILModel.cpp @@ -46,8 +46,9 @@ * TrackerMILModel */ -namespace cv -{ +namespace cv { +inline namespace tracking { +namespace impl { TrackerMILModel::TrackerMILModel( const Rect& boundingBox ) { @@ -122,4 +123,4 @@ void TrackerMILModel::setMode( int trainingMode, const std::vector& samples mode = trainingMode; } -} +}}} // namespace diff --git a/modules/tracking/src/trackerMILModel.hpp b/modules/tracking/src/trackerMILModel.hpp index cdbf9a3cb1c..41945be7aba 100644 --- a/modules/tracking/src/trackerMILModel.hpp +++ b/modules/tracking/src/trackerMILModel.hpp @@ -42,10 +42,9 @@ #ifndef __OPENCV_TRACKER_MIL_MODEL_HPP__ #define __OPENCV_TRACKER_MIL_MODEL_HPP__ -#include "opencv2/core.hpp" - -namespace cv -{ +namespace cv { +inline namespace tracking { +namespace impl { /** * \brief Implementation of TrackerModel for MIL algorithm @@ -98,6 +97,6 @@ class TrackerMILModel : public TrackerModel int height; //initial height of the boundingBox }; -} /* namespace cv */ +}}} // namespace #endif diff --git a/modules/tracking/src/trackerMedianFlow.cpp b/modules/tracking/src/trackerMedianFlow.cpp index 6f41c3ebe6c..f183448e231 100644 --- a/modules/tracking/src/trackerMedianFlow.cpp +++ b/modules/tracking/src/trackerMedianFlow.cpp @@ -40,15 +40,15 @@ //M*/ #include "precomp.hpp" -#include "opencv2/video/tracking.hpp" -#include "opencv2/imgproc.hpp" +#include "opencv2/tracking/tracking_legacy.hpp" + #include "tracking_utils.hpp" #include #include -namespace -{ -using namespace cv; +namespace cv { +inline namespace tracking { +namespace impl { #undef MEDIAN_FLOW_TRACKER_DEBUG_LOGS #ifdef MEDIAN_FLOW_TRACKER_DEBUG_LOGS @@ -72,7 +72,8 @@ using namespace cv; * optimize (allocation<-->reallocation) */ -class TrackerMedianFlowImpl : public TrackerMedianFlow{ +class TrackerMedianFlowImpl : public legacy::TrackerMedianFlow +{ public: TrackerMedianFlowImpl(TrackerMedianFlow::Params paramsIn = TrackerMedianFlow::Params()) {params=paramsIn;isInit=false;} void read( const FileNode& fn ) CV_OVERRIDE; @@ -95,6 +96,7 @@ class TrackerMedianFlowImpl : public TrackerMedianFlow{ TrackerMedianFlow::Params params; }; +static Mat getPatch(Mat image, Size patch_size, Point2f patch_center) { Mat patch; @@ -119,7 +121,7 @@ Mat getPatch(Mat image, Size patch_size, Point2f patch_center) class TrackerMedianFlowModel : public TrackerModel{ public: - TrackerMedianFlowModel(TrackerMedianFlow::Params /*params*/){} + TrackerMedianFlowModel(legacy::TrackerMedianFlow::Params /*params*/){} Rect2d getBoundingBox(){return boundingBox_;} void setBoudingBox(Rect2d boundingBox){boundingBox_=boundingBox;} Mat getImage(){return image_;} @@ -391,10 +393,11 @@ void TrackerMedianFlowImpl::check_NCC(const Mat& oldImage,const Mat& newImage, } } -} /* anonymous namespace */ +}} // namespace + +namespace legacy { +inline namespace tracking { -namespace cv -{ /* * Parameters */ @@ -442,11 +445,13 @@ void TrackerMedianFlow::Params::write( cv::FileStorage& fs ) const{ fs << "maxMedianLengthOfDisplacementDifference" << maxMedianLengthOfDisplacementDifference; } -Ptr TrackerMedianFlow::create(const TrackerMedianFlow::Params ¶meters){ - return Ptr(new TrackerMedianFlowImpl(parameters)); +Ptr TrackerMedianFlow::create(const TrackerMedianFlow::Params ¶meters) +{ + return makePtr(parameters); } -Ptr TrackerMedianFlow::create(){ - return Ptr(new TrackerMedianFlowImpl()); +Ptr TrackerMedianFlow::create() +{ + return create(TrackerMedianFlow::Params()); } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/trackerModel.cpp b/modules/tracking/src/trackerModel.cpp index 023b3b67dc3..065721f3f03 100644 --- a/modules/tracking/src/trackerModel.cpp +++ b/modules/tracking/src/trackerModel.cpp @@ -41,8 +41,9 @@ #include "precomp.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { /* * TrackerModel @@ -174,4 +175,4 @@ void TrackerTargetState::setTargetHeight( int height ) targetHeight = height; } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/trackerSampler.cpp b/modules/tracking/src/trackerSampler.cpp index 38ea52317e3..28f5d0f04fa 100644 --- a/modules/tracking/src/trackerSampler.cpp +++ b/modules/tracking/src/trackerSampler.cpp @@ -41,8 +41,9 @@ #include "precomp.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { /* * TrackerSampler @@ -139,4 +140,5 @@ void TrackerSampler::clearSamples() samples.clear(); } -} /* namespace cv */ + +}}} // namespace diff --git a/modules/tracking/src/trackerSamplerAlgorithm.cpp b/modules/tracking/src/trackerSamplerAlgorithm.cpp index ea52070d59c..39d1271ba79 100644 --- a/modules/tracking/src/trackerSamplerAlgorithm.cpp +++ b/modules/tracking/src/trackerSamplerAlgorithm.cpp @@ -40,18 +40,12 @@ //M*/ #include "precomp.hpp" -#include #include "PFSolver.hpp" #include "TrackingFunctionPF.hpp" -#ifdef _WIN32 -#define TIME( arg ) (((double) clock()) / CLOCKS_PER_SEC) -#else -#define TIME( arg ) (time( arg )) -#endif - -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { /* * TrackerSamplerAlgorithm @@ -114,7 +108,7 @@ TrackerSamplerCSC::TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meter { className = "CSC"; mode = MODE_INIT_POS; - rng = RNG( uint64( TIME( 0 ) ) ); + rng = theRNG(); } @@ -410,4 +404,5 @@ bool TrackerSamplerPF::samplingImpl( const Mat& image, Rect boundingBox, std::ve return true; } -} /* namespace cv */ + +}}} // namespace diff --git a/modules/tracking/src/trackerStateEstimator.cpp b/modules/tracking/src/trackerStateEstimator.cpp index 094aef059e4..d0f9dd36d9e 100644 --- a/modules/tracking/src/trackerStateEstimator.cpp +++ b/modules/tracking/src/trackerStateEstimator.cpp @@ -41,8 +41,9 @@ #include "precomp.hpp" -namespace cv -{ +namespace cv { +namespace detail { +inline namespace tracking { /* * TrackerStateEstimator @@ -441,4 +442,4 @@ void TrackerStateEstimatorSVM::updateImpl( std::vector& /*confide } -} /* namespace cv */ +}}} // namespace diff --git a/modules/tracking/src/tracking_by_matching.cpp b/modules/tracking/src/tracking_by_matching.cpp index 3e2199b5c7a..8b0228306d5 100644 --- a/modules/tracking/src/tracking_by_matching.cpp +++ b/modules/tracking/src/tracking_by_matching.cpp @@ -2,6 +2,8 @@ // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html. +#include "precomp.hpp" + #include #include #include @@ -25,7 +27,11 @@ #define TBM_CHECK_LE(actual, expected) CV_CheckLE(actual, expected, "Assertion error:") #define TBM_CHECK_GE(actual, expected) CV_CheckGE(actual, expected, "Assertion error:") -using namespace cv::tbm; +namespace cv { +namespace detail { +inline namespace tracking { + +using namespace tbm; CosDistance::CosDistance(const cv::Size &descriptor_size) : descriptor_size_(descriptor_size) { @@ -1335,3 +1341,5 @@ void TrackerByMatching::PrintConfusionMatrices() const { std::cout << cm << std::endl << std::endl; } } + +}}} // namespace diff --git a/modules/tracking/src/tracking_utils.cpp b/modules/tracking/src/tracking_utils.cpp index fd6ae1bc5f1..c197f902d6d 100644 --- a/modules/tracking/src/tracking_utils.cpp +++ b/modules/tracking/src/tracking_utils.cpp @@ -2,9 +2,10 @@ // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html. +#include "precomp.hpp" #include "tracking_utils.hpp" -using namespace cv; +namespace cv { double tracking_internal::computeNCC(const Mat& patch1, const Mat& patch2) { @@ -67,3 +68,5 @@ double tracking_internal::computeNCC(const Mat& patch1, const Mat& patch2) return (sq2 == 0) ? sq1 / abs(sq1) : (prod - s1 * s2 / N) / sq1 / sq2; } } + +} // namespace diff --git a/modules/tracking/src/tracking_utils.hpp b/modules/tracking/src/tracking_utils.hpp index a7356224274..903b783b828 100644 --- a/modules/tracking/src/tracking_utils.hpp +++ b/modules/tracking/src/tracking_utils.hpp @@ -3,12 +3,11 @@ // of this distribution and at http://opencv.org/license.html. #ifndef __OPENCV_TRACKING_UTILS_HPP__ -#include "precomp.hpp" #include namespace cv { -namespace tracking_internal -{ +namespace tracking_internal { + /** Computes normalized corellation coefficient between the two patches (they should be * of the same size).*/ double computeNCC(const Mat& patch1, const Mat& patch2); @@ -42,6 +41,6 @@ namespace tracking_internal std::vector copy(values); return getMedianAndDoPartition(copy); } -} -} + +}} // namespace #endif diff --git a/modules/tracking/src/unscented_kalman.cpp b/modules/tracking/src/unscented_kalman.cpp index 9b800160465..d6d82a7f3bf 100644 --- a/modules/tracking/src/unscented_kalman.cpp +++ b/modules/tracking/src/unscented_kalman.cpp @@ -42,10 +42,10 @@ #include "precomp.hpp" #include "opencv2/tracking/kalman_filters.hpp" -namespace cv -{ -namespace tracking -{ +namespace cv { +namespace detail { +inline namespace tracking { +inline namespace kalman_filters { void UnscentedKalmanFilterParams:: init( int dp, int mp, int cp, double processNoiseCovDiag, double measurementNoiseCovDiag, @@ -369,5 +369,4 @@ Ptr createUnscentedKalmanFilter(const UnscentedKalmanFilt return kfu; } -} // tracking -} // cv +}}}} // namespace diff --git a/modules/tracking/test/test_aukf.cpp b/modules/tracking/test/test_aukf.cpp index 73372971c06..32e32c96e6f 100644 --- a/modules/tracking/test/test_aukf.cpp +++ b/modules/tracking/test/test_aukf.cpp @@ -43,7 +43,7 @@ #include "opencv2/tracking/kalman_filters.hpp" namespace opencv_test { namespace { -using namespace cv::tracking; +using namespace cv::detail; // In this two tests Augmented Unscented Kalman Filter are applied to the dynamic system from example "The reentry problem" from // "A New Extension of the Kalman Filter to Nonlinear Systems" by Simon J. Julier and Jeffrey K. Uhlmann. diff --git a/modules/tracking/test/test_trackerParametersIO.cpp b/modules/tracking/test/test_trackerParametersIO.cpp index ebe43a5fb7d..59e7917e271 100644 --- a/modules/tracking/test/test_trackerParametersIO.cpp +++ b/modules/tracking/test/test_trackerParametersIO.cpp @@ -4,11 +4,15 @@ #include "test_precomp.hpp" +#include +//using namespace cv::tracking::legacy; + namespace opencv_test { namespace { + TEST(MEDIAN_FLOW_Parameters, IO) { - TrackerMedianFlow::Params parameters; + legacy::TrackerMedianFlow::Params parameters; parameters.maxLevel = 10; parameters.maxMedianLengthOfDisplacementDifference = 11; @@ -25,7 +29,7 @@ TEST(MEDIAN_FLOW_Parameters, IO) FileStorage fsReader(serializedParameters, FileStorage::READ + FileStorage::MEMORY); - TrackerMedianFlow::Params readParameters; + legacy::TrackerMedianFlow::Params readParameters; readParameters.read(fsReader.root()); ASSERT_EQ(parameters.maxLevel, readParameters.maxLevel); @@ -41,11 +45,11 @@ TEST(MEDIAN_FLOW_Parameters, IO) TEST(MEDIAN_FLOW_Parameters, Default_Value_If_Absent) { - TrackerMedianFlow::Params defaultParameters; + legacy::TrackerMedianFlow::Params defaultParameters; FileStorage fsReader(String("%YAML 1.0"), FileStorage::READ + FileStorage::MEMORY); - TrackerMedianFlow::Params readParameters; + legacy::TrackerMedianFlow::Params readParameters; readParameters.read(fsReader.root()); ASSERT_EQ(defaultParameters.maxLevel, readParameters.maxLevel); @@ -60,7 +64,7 @@ TEST(MEDIAN_FLOW_Parameters, Default_Value_If_Absent) TEST(KCF_Parameters, IO) { - TrackerKCF::Params parameters; + legacy::TrackerKCF::Params parameters; parameters.sigma = 0.3f; parameters.lambda = 0.02f; @@ -83,7 +87,7 @@ TEST(KCF_Parameters, IO) FileStorage fsReader(serializedParameters, FileStorage::READ + FileStorage::MEMORY); - TrackerKCF::Params readParameters; + legacy::TrackerKCF::Params readParameters; readParameters.read(fsReader.root()); ASSERT_DOUBLE_EQ(parameters.sigma, readParameters.sigma); @@ -103,11 +107,11 @@ TEST(KCF_Parameters, IO) TEST(KCF_Parameters, Default_Value_If_Absent) { - TrackerKCF::Params defaultParameters; + legacy::TrackerKCF::Params defaultParameters; FileStorage fsReader(String("%YAML 1.0"), FileStorage::READ + FileStorage::MEMORY); - TrackerKCF::Params readParameters; + legacy::TrackerKCF::Params readParameters; readParameters.read(fsReader.root()); ASSERT_DOUBLE_EQ(defaultParameters.sigma, readParameters.sigma); diff --git a/modules/tracking/test/test_trackers.cpp b/modules/tracking/test/test_trackers.cpp index db1617e3500..222d272ca2b 100644 --- a/modules/tracking/test/test_trackers.cpp +++ b/modules/tracking/test/test_trackers.cpp @@ -41,7 +41,16 @@ #include "test_precomp.hpp" +#define TEST_LEGACY +#include + +//#define DEBUG_TEST +#ifdef DEBUG_TEST +#include +#endif + namespace opencv_test { namespace { +//using namespace cv::tracking; #define TESTSET_NAMES testing::Values("david","dudek","faceocc2") @@ -73,19 +82,52 @@ enum BBTransformations Scale_1_2 = 12 }; +namespace { + +std::vector splitString(const std::string& s_, const std::string& delimiter) +{ + std::string s = s_; + std::vector token; + size_t pos = 0; + while ( ( pos = s.find( delimiter ) ) != std::string::npos ) + { + token.push_back( s.substr( 0, pos ) ); + s.erase( 0, pos + delimiter.length() ); + } + token.push_back( s ); + return token; +} + +float calcDistance(const Rect& a, const Rect& b) +{ + Point2f p_a( (float)(a.x + a.width / 2), (float)(a.y + a.height / 2) ); + Point2f p_b( (float)(b.x + b.width / 2), (float)(b.y + b.height / 2) ); + return sqrt( pow( p_a.x - p_b.x, 2 ) + pow( p_a.y - p_b.y, 2 ) ); +} + +float calcOverlap(const Rect& a, const Rect& b) +{ + float rectIntersectionArea = (float)(a & b).area(); + return rectIntersectionArea / (a.area() + b.area() - rectIntersectionArea); +} + +} // namespace + + +template class TrackerTest { - public: +public: - TrackerTest(Ptr _tracker, string _video, float _distanceThreshold, - float _overlapThreshold, int _shift = NoTransform, int _segmentIdx = 1, int _numSegments = 10 ); - virtual ~TrackerTest(); - virtual void run(); + TrackerTest(const Ptr& tracker, const string& video, float distanceThreshold, + float overlapThreshold, int shift = NoTransform, int segmentIdx = 1, int numSegments = 10); + ~TrackerTest() {} + void run(); - protected: +protected: void checkDataTest(); - void distanceAndOvrerlapTest(); + void distanceAndOverlapTest(); Ptr tracker; string video; @@ -103,16 +145,13 @@ class TrackerTest int endFrame; vector validSequence; - private: - float calcDistance( Rect a, Rect b ); - float calcOverlap( Rect a, Rect b ); - Rect applyShift(Rect bb); - std::vector splitString( std::string s, std::string delimiter ); - +private: + Rect applyShift(const Rect& bb); }; -TrackerTest::TrackerTest(Ptr _tracker, string _video, float _distanceThreshold, - float _overlapThreshold, int _shift, int _segmentIdx, int _numSegments ) : +template +TrackerTest::TrackerTest(const Ptr& _tracker, const string& _video, float _distanceThreshold, + float _overlapThreshold, int _shift, int _segmentIdx, int _numSegments ) : tracker( _tracker ), video( _video ), overlapThreshold( _overlapThreshold ), @@ -121,41 +160,13 @@ TrackerTest::TrackerTest(Ptr _tracker, string _video, float _distanceTh shift(_shift), numSegments(_numSegments) { + // nothing } -TrackerTest::~TrackerTest() -{ - -} - -std::vector TrackerTest::splitString( std::string s, std::string delimiter ) -{ - std::vector token; - size_t pos = 0; - while ( ( pos = s.find( delimiter ) ) != std::string::npos ) - { - token.push_back( s.substr( 0, pos ) ); - s.erase( 0, pos + delimiter.length() ); - } - token.push_back( s ); - return token; -} - -float TrackerTest::calcDistance( Rect a, Rect b ) -{ - Point2f p_a( (float)(a.x + a.width / 2), (float)(a.y + a.height / 2) ); - Point2f p_b( (float)(b.x + b.width / 2), (float)(b.y + b.height / 2) ); - return sqrt( pow( p_a.x - p_b.x, 2 ) + pow( p_a.y - p_b.y, 2 ) ); -} - -float TrackerTest::calcOverlap( Rect a, Rect b ) -{ - float rectIntersectionArea = (float)(a & b).area(); - return rectIntersectionArea / (a.area() + b.area() - rectIntersectionArea); -} - -Rect TrackerTest::applyShift(Rect bb) +template +Rect TrackerTest::applyShift(const Rect& bb_) { + Rect bb = bb_; Point center( bb.x + ( bb.width / 2 ), bb.y + ( bb.height / 2 ) ); int xLimit = bb.x + bb.width - 1; @@ -244,51 +255,77 @@ Rect TrackerTest::applyShift(Rect bb) return bb; } -void TrackerTest::distanceAndOvrerlapTest() +template +void TrackerTest::distanceAndOverlapTest() { - Mat frame; bool initialized = false; int fc = ( startFrame - gtStartFrame ); bbs.at( fc ) = applyShift(bbs.at( fc )); Rect currentBBi = bbs.at( fc ); - Rect2d currentBB(currentBBi); + ROI_t currentBB(currentBBi); float sumDistance = 0; float sumOverlap = 0; string folder = cvtest::TS::ptr()->get_data_path() + "/" + TRACKING_DIR + "/" + video + "/" + FOLDER_IMG; + string videoPath = folder + "/" + video + ".webm"; VideoCapture c; - c.open( folder + "/" + video + ".webm" ); - c.set( CAP_PROP_POS_FRAMES, startFrame ); + c.open(videoPath); + ASSERT_TRUE(c.isOpened()) << videoPath; +#if 0 + c.set(CAP_PROP_POS_FRAMES, startFrame); +#else + if (startFrame) + std::cout << "startFrame = " << startFrame << std::endl; + for (int i = 0; i < startFrame; i++) + { + Mat dummy_frame; + c >> dummy_frame; + ASSERT_FALSE(dummy_frame.empty()) << i << ": " << videoPath; + } +#endif for ( int frameCounter = startFrame; frameCounter < endFrame; frameCounter++ ) { + Mat frame; c >> frame; - if( frame.empty() ) - { - break; - } + ASSERT_FALSE(frame.empty()) << "frameCounter=" << frameCounter << " video=" << videoPath; if( !initialized ) { +#if 0 if( !tracker->init( frame, currentBB ) ) { FAIL()<< "Could not initialize tracker" << endl; return; } +#else + tracker->init(frame, currentBB); +#endif + std::cout << "frame size = " << frame.size() << std::endl; initialized = true; } else if( initialized ) { if( frameCounter >= (int) bbs.size() ) - break; + break; tracker->update( frame, currentBB ); } float curDistance = calcDistance( currentBB, bbs.at( fc ) ); float curOverlap = calcOverlap( currentBB, bbs.at( fc ) ); +#ifdef DEBUG_TEST + Mat result; + repeat(frame, 1, 2, result); + rectangle(result, currentBB, Scalar(0, 255, 0), 1); + Rect roi2(frame.cols, 0, frame.cols, frame.rows); + rectangle(result(roi2), bbs.at(fc), Scalar(0, 0, 255), 1); + imshow("result", result); + waitKey(1); +#endif + sumDistance += curDistance; sumOverlap += curOverlap; fc++; @@ -297,20 +334,12 @@ void TrackerTest::distanceAndOvrerlapTest() float meanDistance = sumDistance / (endFrame - startFrame); float meanOverlap = sumOverlap / (endFrame - startFrame); - if( meanDistance > distanceThreshold ) - { - FAIL()<< "Incorrect distance: curr = " << meanDistance << ", max = " << distanceThreshold << endl; - return; - } - - if( meanOverlap < overlapThreshold ) - { - FAIL()<< "Incorrect overlap: curr = " << meanOverlap << ", min = " << overlapThreshold << endl; - return; - } + EXPECT_LE(meanDistance, distanceThreshold); + EXPECT_GE(meanOverlap, overlapThreshold); } -void TrackerTest::checkDataTest() +template +void TrackerTest::checkDataTest() { FileStorage fs; @@ -324,10 +353,7 @@ void TrackerTest::checkDataTest() std::ifstream gt; //open the ground truth gt.open( gtFile.c_str() ); - if( !gt.is_open() ) - { - FAIL()<< "Ground truth file " << gtFile << " can not be read" << endl; - } + ASSERT_TRUE(gt.is_open()) << gtFile; string line; int bbCounter = 0; while ( getline( gt, line ) ) @@ -372,20 +398,14 @@ void TrackerTest::checkDataTest() std::ifstream gt2; //open the ground truth gt2.open( gtFile.c_str() ); - if( !gt2.is_open() ) - { - FAIL()<< "Ground truth file " << gtFile << " can not be read" << endl; - } + ASSERT_TRUE(gt2.is_open()) << gtFile; string line2; int bbCounter2 = 0; while ( getline( gt2, line2 ) ) { vector tokens = splitString( line2, "," ); Rect bb( atoi( tokens.at( 0 ).c_str() ), atoi( tokens.at( 1 ).c_str() ), atoi( tokens.at( 2 ).c_str() ), atoi( tokens.at( 3 ).c_str() ) ); - if( tokens.size() != 4 ) - { - FAIL()<< "Incorrect ground truth file"; - } + ASSERT_EQ((size_t)4, tokens.size()) << "Incorrect ground truth file " << gtFile; bbs.push_back( bb ); bbCounter2++; @@ -396,17 +416,12 @@ void TrackerTest::checkDataTest() endFrame = (int)bbs.size(); } -void TrackerTest::run() +template +void TrackerTest::run() { - srand( 1 ); - - SCOPED_TRACE( "A" ); + srand( 1 ); // FIXIT remove that, ensure that there is no "rand()" in implementation - if( !tracker ) - { - FAIL()<< "Error in the instantiation of the tracker" << endl; - return; - } + ASSERT_TRUE(tracker); checkDataTest(); @@ -414,7 +429,7 @@ void TrackerTest::run() if( ::testing::Test::HasFatalFailure() ) return; - distanceAndOvrerlapTest(); + distanceAndOverlapTest(); } /****************************************************************************************\ @@ -433,167 +448,240 @@ PARAM_TEST_CASE(DistanceAndOverlap, string) TEST_P(DistanceAndOverlap, MedianFlow) { - TrackerTest test( TrackerMedianFlow::create(), dataset, 35, .5f, NoTransform, 1, 1); + TrackerTest test(legacy::TrackerMedianFlow::create(), dataset, 35, .5f, NoTransform, 1, 1); test.run(); } TEST_P(DistanceAndOverlap, MIL) { - TrackerTest test( TrackerMIL::create(), dataset, 30, .65f, NoTransform); + TrackerTest test(TrackerMIL::create(), dataset, 30, .65f, NoTransform); + test.run(); +} +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, MIL_legacy) +{ + TrackerTest test(legacy::TrackerMIL::create(), dataset, 30, .65f, NoTransform); test.run(); } +#endif TEST_P(DistanceAndOverlap, Boosting) { - TrackerTest test( TrackerBoosting::create(), dataset, 70, .7f, NoTransform); + TrackerTest test(legacy::TrackerBoosting::create(), dataset, 70, .7f, NoTransform); test.run(); } TEST_P(DistanceAndOverlap, KCF) { - TrackerTest test( TrackerKCF::create(), dataset, 20, .35f, NoTransform, 5); + TrackerTest test(TrackerKCF::create(), dataset, 20, .35f, NoTransform, 5); test.run(); } +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, KCF_legacy) +{ + TrackerTest test(legacy::TrackerKCF::create(), dataset, 20, .35f, NoTransform, 5); + test.run(); +} +#endif TEST_P(DistanceAndOverlap, TLD) { - TrackerTest test( TrackerTLD::create(), dataset, 40, .45f, NoTransform); + TrackerTest test(legacy::TrackerTLD::create(), dataset, 40, .45f, NoTransform); test.run(); } TEST_P(DistanceAndOverlap, MOSSE) { - TrackerTest test( TrackerMOSSE::create(), dataset, 22, .7f, NoTransform); + TrackerTest test(legacy::TrackerMOSSE::create(), dataset, 22, .7f, NoTransform); test.run(); } TEST_P(DistanceAndOverlap, CSRT) { - TrackerTest test( TrackerCSRT::create(), dataset, 22, .7f, NoTransform); + TrackerTest test(TrackerCSRT::create(), dataset, 22, .7f, NoTransform); test.run(); } +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, CSRT_legacy) +{ + TrackerTest test(legacy::TrackerCSRT::create(), dataset, 22, .7f, NoTransform); + test.run(); +} +#endif /***************************************************************************************/ //Tests with shifted initial window TEST_P(DistanceAndOverlap, Shifted_Data_MedianFlow) { - TrackerTest test( TrackerMedianFlow::create(), dataset, 80, .2f, CenterShiftLeft, 1, 1); + TrackerTest test(legacy::TrackerMedianFlow::create(), dataset, 80, .2f, CenterShiftLeft, 1, 1); test.run(); } TEST_P(DistanceAndOverlap, Shifted_Data_MIL) { - TrackerTest test( TrackerMIL::create(), dataset, 30, .6f, CenterShiftLeft); + TrackerTest test(TrackerMIL::create(), dataset, 30, .6f, CenterShiftLeft); test.run(); } +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, Shifted_Data_MIL_legacy) +{ + TrackerTest test(legacy::TrackerMIL::create(), dataset, 30, .6f, CenterShiftLeft); + test.run(); +} +#endif TEST_P(DistanceAndOverlap, Shifted_Data_Boosting) { - TrackerTest test( TrackerBoosting::create(), dataset, 80, .65f, CenterShiftLeft); + TrackerTest test(legacy::TrackerBoosting::create(), dataset, 80, .65f, CenterShiftLeft); test.run(); } TEST_P(DistanceAndOverlap, Shifted_Data_KCF) { - TrackerTest test( TrackerKCF::create(), dataset, 20, .4f, CenterShiftLeft, 5); + TrackerTest test(TrackerKCF::create(), dataset, 20, .4f, CenterShiftLeft, 5); + test.run(); +} +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, Shifted_Data_KCF_legacy) +{ + TrackerTest test(legacy::TrackerKCF::create(), dataset, 20, .4f, CenterShiftLeft, 5); test.run(); } +#endif TEST_P(DistanceAndOverlap, Shifted_Data_TLD) { - TrackerTest test( TrackerTLD::create(), dataset, 30, .35f, CenterShiftLeft); + TrackerTest test(legacy::TrackerTLD::create(), dataset, 30, .35f, CenterShiftLeft); test.run(); } TEST_P(DistanceAndOverlap, Shifted_Data_MOSSE) { - TrackerTest test( TrackerMOSSE::create(), dataset, 13, .69f, CenterShiftLeft); + TrackerTest test(legacy::TrackerMOSSE::create(), dataset, 13, .69f, CenterShiftLeft); test.run(); } TEST_P(DistanceAndOverlap, Shifted_Data_CSRT) { - TrackerTest test( TrackerCSRT::create(), dataset, 13, .69f, CenterShiftLeft); + TrackerTest test(TrackerCSRT::create(), dataset, 13, .69f, CenterShiftLeft); + test.run(); +} +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, Shifted_Data_CSRT_legacy) +{ + TrackerTest test(legacy::TrackerCSRT::create(), dataset, 13, .69f, CenterShiftLeft); test.run(); } +#endif + /***************************************************************************************/ //Tests with scaled initial window TEST_P(DistanceAndOverlap, Scaled_Data_MedianFlow) { - TrackerTest test( TrackerMedianFlow::create(), dataset, 25, .5f, Scale_1_1, 1, 1); + TrackerTest test(legacy::TrackerMedianFlow::create(), dataset, 25, .5f, Scale_1_1, 1, 1); test.run(); } TEST_P(DistanceAndOverlap, Scaled_Data_MIL) { - TrackerTest test( TrackerMIL::create(), dataset, 30, .7f, Scale_1_1); + TrackerTest test(TrackerMIL::create(), dataset, 30, .7f, Scale_1_1); + test.run(); +} +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, Scaled_Data_MIL_legacy) +{ + TrackerTest test(legacy::TrackerMIL::create(), dataset, 30, .7f, Scale_1_1); test.run(); } +#endif TEST_P(DistanceAndOverlap, Scaled_Data_Boosting) { - TrackerTest test( TrackerBoosting::create(), dataset, 80, .7f, Scale_1_1); + TrackerTest test(legacy::TrackerBoosting::create(), dataset, 80, .7f, Scale_1_1); test.run(); } TEST_P(DistanceAndOverlap, Scaled_Data_KCF) { - TrackerTest test( TrackerKCF::create(), dataset, 20, .4f, Scale_1_1, 5); + TrackerTest test(TrackerKCF::create(), dataset, 20, .4f, Scale_1_1, 5); test.run(); } - -TEST_P(DistanceAndOverlap, Scaled_Data_TLD) +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, Scaled_Data_KCF_legacy) { - TrackerTest test( TrackerTLD::create(), dataset, 30, .45f, Scale_1_1); + TrackerTest test(legacy::TrackerKCF::create(), dataset, 20, .4f, Scale_1_1, 5); test.run(); } +#endif - -TEST_P(DistanceAndOverlap, DISABLED_GOTURN) +TEST_P(DistanceAndOverlap, Scaled_Data_TLD) { - TrackerTest test(TrackerGOTURN::create(), dataset, 18, .5f, NoTransform); + TrackerTest test(legacy::TrackerTLD::create(), dataset, 30, .45f, Scale_1_1); test.run(); } TEST_P(DistanceAndOverlap, Scaled_Data_MOSSE) { - TrackerTest test( TrackerMOSSE::create(), dataset, 22, 0.69f, Scale_1_1, 1); + TrackerTest test(legacy::TrackerMOSSE::create(), dataset, 22, 0.69f, Scale_1_1, 1); test.run(); } TEST_P(DistanceAndOverlap, Scaled_Data_CSRT) { - TrackerTest test( TrackerCSRT::create(), dataset, 22, 0.69f, Scale_1_1, 1); + TrackerTest test(TrackerCSRT::create(), dataset, 22, 0.69f, Scale_1_1, 1); + test.run(); +} +#ifdef TEST_LEGACY +TEST_P(DistanceAndOverlap, Scaled_Data_CSRT_legacy) +{ + TrackerTest test(TrackerCSRT::create(), dataset, 22, 0.69f, Scale_1_1, 1); test.run(); } +#endif + +TEST_P(DistanceAndOverlap, GOTURN) +{ + std::string model = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.prototxt"); + std::string weights = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.caffemodel", false); + cv::TrackerGOTURN::Params params; + params.modelTxt = model; + params.modelBin = weights; + TrackerTest test(TrackerGOTURN::create(params), dataset, 35, .35f, NoTransform); + test.run(); +} + +INSTANTIATE_TEST_CASE_P(Tracking, DistanceAndOverlap, TESTSET_NAMES); + + TEST(GOTURN, memory_usage) { - cv::Rect2d roi(145, 70, 85, 85); - cv::Mat frame; + cv::Rect roi(145, 70, 85, 85); std::string model = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.prototxt"); std::string weights = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.caffemodel", false); cv::TrackerGOTURN::Params params; params.modelTxt = model; params.modelBin = weights; - cv::Ptr tracker = cv::TrackerGOTURN::create(params); + cv::Ptr tracker = TrackerGOTURN::create(params); + string inputVideo = cvtest::findDataFile("tracking/david/data/david.webm"); cv::VideoCapture video(inputVideo); + ASSERT_TRUE(video.isOpened()) << inputVideo; + cv::Mat frame; video >> frame; + ASSERT_FALSE(frame.empty()) << inputVideo; tracker->init(frame, roi); string ground_truth_bb; for (int nframes = 0; nframes < 15; ++nframes) { std::cout << "Frame: " << nframes << std::endl; video >> frame; - tracker->update(frame, roi); + bool res = tracker->update(frame, roi); + ASSERT_TRUE(res); std::cout << "Predicted ROI: " << roi << std::endl; } } -INSTANTIATE_TEST_CASE_P( Tracking, DistanceAndOverlap, TESTSET_NAMES); - }} // namespace -/* End of file. */ diff --git a/modules/tracking/test/test_ukf.cpp b/modules/tracking/test/test_ukf.cpp index 60196ee1d36..c8ebe3edc1d 100644 --- a/modules/tracking/test/test_ukf.cpp +++ b/modules/tracking/test/test_ukf.cpp @@ -43,7 +43,7 @@ #include "opencv2/tracking/kalman_filters.hpp" namespace opencv_test { namespace { -using namespace cv::tracking; +using namespace cv::detail; // In this two tests Unscented Kalman Filter are applied to the dynamic system from example "The reentry problem" from // "A New Extension of the Kalman Filter to Nonlinear Systems" by Simon J. Julier and Jeffrey K. Uhlmann. diff --git a/modules/tracking/tutorials/tutorial_multitracker.markdown b/modules/tracking/tutorials/tutorial_multitracker.markdown index f9c7605a53d..0dce97fc4b7 100644 --- a/modules/tracking/tutorials/tutorial_multitracker.markdown +++ b/modules/tracking/tutorials/tutorial_multitracker.markdown @@ -38,11 +38,11 @@ Explanation You can add all tracked objects at once to the MultiTracker as shown in the code. In this case, all objects will be tracked using same tracking algorithm as specified in decaration of MultiTracker object. If you want to use different tracker algorithms for each tracked object, - You should add the tracked objects one by one and specify their tracking algorithm using the variant of @ref cv::MultiTracker::add. - @sa cv::MultiTracker::add( const String& trackerType, const Mat& image, const Rect2d& boundingBox ) + You should add the tracked objects one by one and specify their tracking algorithm using the variant of @ref cv::legacy::MultiTracker::add. + @sa cv::legacy::MultiTracker::add( const String& trackerType, const Mat& image, const Rect2d& boundingBox ) -# **Obtaining the result** @snippet tracking/samples/tutorial_multitracker.cpp result - You can access the result from the public variable @ref cv::MultiTracker::objects provided by the MultiTracker class as shown in the code. + You can access the result from the public variable @ref cv::legacy::MultiTracker::objects provided by the MultiTracker class as shown in the code. From 1d84f2820243564bb376f22c6a1ff8640027a0fa Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 17 Nov 2020 10:58:58 +0100 Subject: [PATCH 21/29] phase_unwrapping: add asserts, documentation for input types --- .../include/opencv2/phase_unwrapping/phase_unwrapping.hpp | 4 ++-- modules/phase_unwrapping/src/histogramphaseunwrapping.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/phase_unwrapping/include/opencv2/phase_unwrapping/phase_unwrapping.hpp b/modules/phase_unwrapping/include/opencv2/phase_unwrapping/phase_unwrapping.hpp index 5b5cb5100ba..1091ff38029 100644 --- a/modules/phase_unwrapping/include/opencv2/phase_unwrapping/phase_unwrapping.hpp +++ b/modules/phase_unwrapping/include/opencv2/phase_unwrapping/phase_unwrapping.hpp @@ -58,9 +58,9 @@ class CV_EXPORTS_W PhaseUnwrapping : public virtual Algorithm /** * @brief Unwraps a 2D phase map. - * @param wrappedPhaseMap The wrapped phase map that needs to be unwrapped. + * @param wrappedPhaseMap The wrapped phase map of type CV_32FC1 that needs to be unwrapped. * @param unwrappedPhaseMap The unwrapped phase map. - * @param shadowMask Optional parameter used when some pixels do not hold any phase information in the wrapped phase map. + * @param shadowMask Optional CV_8UC1 mask image used when some pixels do not hold any phase information in the wrapped phase map. */ CV_WRAP virtual void unwrapPhaseMap( InputArray wrappedPhaseMap, OutputArray unwrappedPhaseMap, diff --git a/modules/phase_unwrapping/src/histogramphaseunwrapping.cpp b/modules/phase_unwrapping/src/histogramphaseunwrapping.cpp index 591deaf8dd8..c2b3030ac26 100644 --- a/modules/phase_unwrapping/src/histogramphaseunwrapping.cpp +++ b/modules/phase_unwrapping/src/histogramphaseunwrapping.cpp @@ -402,6 +402,9 @@ void HistogramPhaseUnwrapping_Impl::unwrapPhaseMap( InputArray wrappedPhaseMap, temp.copyTo(mask); } + CV_CheckTypeEQ(wPhaseMap.type(), CV_32FC1, ""); + CV_CheckTypeEQ(mask.type(), CV_8UC1, ""); + computePixelsReliability(wPhaseMap, mask); computeEdgesReliabilityAndCreateHistogram(); From a51b1b1e9442f65c59f60a0c937cfa64482eadbe Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Mon, 16 Nov 2020 23:40:01 +0000 Subject: [PATCH 22/29] tracking: move Tracking API to the main repository --- modules/tracking/CMakeLists.txt | 4 + modules/tracking/doc/tracking.bib | 38 -- modules/tracking/include/opencv2/tracking.hpp | 114 +--- .../include/opencv2/tracking/feature.hpp | 2 +- .../include/opencv2/tracking/onlineMIL.hpp | 119 ---- .../opencv2/tracking/tracking_internals.hpp | 547 ++++-------------- .../opencv2/tracking/tracking_legacy.hpp | 8 +- .../misc/java/test/TrackerCreateTest.java | 7 +- .../misc/python/pyopencv_tracking.hpp | 2 - modules/tracking/perf/perf_trackers.cpp | 17 - modules/tracking/src/gtrTracker.cpp | 177 ------ .../tracking/src/legacy/tracker.legacy.hpp | 16 +- .../src/legacy/trackerCSRT.legacy.hpp | 4 +- .../tracking/src/legacy/trackerKCF.legacy.hpp | 4 +- modules/tracking/src/onlineMIL.cpp | 381 ------------ modules/tracking/src/tracker.cpp | 16 +- modules/tracking/src/trackerBoosting.cpp | 24 +- modules/tracking/src/trackerFeature.cpp | 38 +- modules/tracking/src/trackerFeatureSet.cpp | 24 +- modules/tracking/src/trackerMIL.cpp | 265 --------- modules/tracking/src/trackerMILModel.cpp | 126 ---- modules/tracking/src/trackerMILModel.hpp | 102 ---- ...erMIL.legacy.hpp => trackerMIL_legacy.cpp} | 26 +- modules/tracking/src/trackerModel.cpp | 178 ------ modules/tracking/src/trackerSampler.cpp | 20 +- .../tracking/src/trackerSamplerAlgorithm.cpp | 28 +- .../tracking/src/trackerStateEstimator.cpp | 181 +----- modules/tracking/test/test_trackers.cpp | 457 +-------------- 28 files changed, 222 insertions(+), 2703 deletions(-) delete mode 100644 modules/tracking/include/opencv2/tracking/onlineMIL.hpp delete mode 100644 modules/tracking/src/gtrTracker.cpp delete mode 100644 modules/tracking/src/onlineMIL.cpp delete mode 100644 modules/tracking/src/trackerMIL.cpp delete mode 100644 modules/tracking/src/trackerMILModel.cpp delete mode 100644 modules/tracking/src/trackerMILModel.hpp rename modules/tracking/src/{legacy/trackerMIL.legacy.hpp => trackerMIL_legacy.cpp} (83%) delete mode 100644 modules/tracking/src/trackerModel.cpp diff --git a/modules/tracking/CMakeLists.txt b/modules/tracking/CMakeLists.txt index 2834b10ca81..c638fcf6f4b 100644 --- a/modules/tracking/CMakeLists.txt +++ b/modules/tracking/CMakeLists.txt @@ -22,3 +22,7 @@ ocv_define_module(tracking ) ocv_warnings_disable(CMAKE_CXX_FLAGS -Wno-shadow /wd4458) + +if(TARGET opencv_test_${name}) + ocv_target_include_directories(opencv_test_${name} "${OpenCV_SOURCE_DIR}/modules") # use common files from video tests +endif() diff --git a/modules/tracking/doc/tracking.bib b/modules/tracking/doc/tracking.bib index 9853b965f5b..78ce6c32fa1 100644 --- a/modules/tracking/doc/tracking.bib +++ b/modules/tracking/doc/tracking.bib @@ -1,12 +1,3 @@ -@inproceedings{MIL, - title={Visual tracking with online multiple instance learning}, - author={Babenko, Boris and Yang, Ming-Hsuan and Belongie, Serge}, - booktitle={Computer Vision and Pattern Recognition, 2009. CVPR 2009. IEEE Conference on}, - pages={983--990}, - year={2009}, - organization={IEEE} -} - @inproceedings{OLB, title={Real-Time Tracking via On-line Boosting.}, author={Grabner, Helmut and Grabner, Michael and Bischof, Horst}, @@ -37,28 +28,6 @@ @article{TLD publisher={IEEE} } -@article{AAM, - title={Adaptive appearance modeling for video tracking: survey and evaluation}, - author={Salti, Samuele and Cavallaro, Andrea and Di Stefano, Luigi}, - journal={Image Processing, IEEE Transactions on}, - volume={21}, - number={10}, - pages={4334--4348}, - year={2012}, - publisher={IEEE} -} - -@article{AMVOT, - title={A survey of appearance models in visual object tracking}, - author={Li, Xi and Hu, Weiming and Shen, Chunhua and Zhang, Zhongfei and Dick, Anthony and Hengel, Anton Van Den}, - journal={ACM Transactions on Intelligent Systems and Technology (TIST)}, - volume={4}, - number={4}, - pages={58}, - year={2013}, - publisher={ACM} -} - @inproceedings{OOT, title={Online object tracking: A benchmark}, author={Wu, Yi and Lim, Jongwoo and Yang, Ming-Hsuan}, @@ -94,13 +63,6 @@ @INPROCEEDINGS{KCF_CN doi={10.1109/CVPR.2014.143}, } -@inproceedings{GOTURN, -title={Learning to Track at 100 FPS with Deep Regression Networks}, -author={Held, David and Thrun, Sebastian and Savarese, Silvio}, -booktitle = {European Conference Computer Vision (ECCV)}, -year = {2016} -} - @inproceedings{MOSSE, title={Visual Object Tracking using Adaptive Correlation Filters}, author={Bolme, David S. and Beveridge, J. Ross and Draper, Bruce A. and Lui Yui, Man}, diff --git a/modules/tracking/include/opencv2/tracking.hpp b/modules/tracking/include/opencv2/tracking.hpp index f9d1b154f50..f8e73e51f28 100644 --- a/modules/tracking/include/opencv2/tracking.hpp +++ b/modules/tracking/include/opencv2/tracking.hpp @@ -6,6 +6,7 @@ #define OPENCV_CONTRIB_TRACKING_HPP #include "opencv2/core.hpp" +#include "opencv2/video/tracking.hpp" namespace cv { #ifndef CV_DOXYGEN @@ -26,37 +27,6 @@ The development in this area is very fragmented and this API is an interface use */ - -/** @brief Base abstract class for the long-term tracker - */ -class CV_EXPORTS_W Tracker -{ -protected: - Tracker(); -public: - virtual ~Tracker(); - - /** @brief Initialize the tracker with a known bounding box that surrounded the target - @param image The initial frame - @param boundingBox The initial bounding box - */ - CV_WRAP virtual - void init(InputArray image, const Rect& boundingBox) = 0; - - /** @brief Update the tracker, find the new most likely bounding box for the target - @param image The current frame - @param boundingBox The bounding box that represent the new target location, if true was returned, not - modified otherwise - - @return True means that target was located and false means that tracker cannot locate target in - current frame. Note, that latter *does not* imply that tracker has failed, maybe target is indeed - missing from the frame (say, out of sight) - */ - CV_WRAP virtual - bool update(InputArray image, CV_OUT Rect& boundingBox) = 0; -}; - - /** @brief the CSRT tracker The implementation is based on @cite Lukezic_IJCV2018 Discriminative Correlation Filter with Channel and Spatial Reliability @@ -117,7 +87,6 @@ class CV_EXPORTS_W TrackerCSRT : public Tracker }; - /** @brief the KCF (Kernelized Correlation Filter) tracker * KCF is a novel tracking framework that utilizes properties of circulant matrix to enhance the processing speed. @@ -180,87 +149,6 @@ class CV_EXPORTS_W TrackerKCF : public Tracker }; - -/** @brief The MIL algorithm trains a classifier in an online manner to separate the object from the -background. - -Multiple Instance Learning avoids the drift problem for a robust tracking. The implementation is -based on @cite MIL . - -Original code can be found here - */ -class CV_EXPORTS_W TrackerMIL : public Tracker -{ -protected: - TrackerMIL(); // use ::create() -public: - virtual ~TrackerMIL() CV_OVERRIDE; - - struct CV_EXPORTS_W_SIMPLE Params - { - CV_WRAP Params(); - //parameters for sampler - CV_PROP_RW float samplerInitInRadius; //!< radius for gathering positive instances during init - CV_PROP_RW int samplerInitMaxNegNum; //!< # negative samples to use during init - CV_PROP_RW float samplerSearchWinSize; //!< size of search window - CV_PROP_RW float samplerTrackInRadius; //!< radius for gathering positive instances during tracking - CV_PROP_RW int samplerTrackMaxPosNum; //!< # positive samples to use during tracking - CV_PROP_RW int samplerTrackMaxNegNum; //!< # negative samples to use during tracking - CV_PROP_RW int featureSetNumFeatures; //!< # features - }; - - /** @brief Create MIL tracker instance - * @param parameters MIL parameters TrackerMIL::Params - */ - static CV_WRAP - Ptr create(const TrackerMIL::Params ¶meters = TrackerMIL::Params()); - - - //void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; - //bool update(InputArray image, CV_OUT Rect& boundingBox) CV_OVERRIDE; -}; - - - -/** @brief the GOTURN (Generic Object Tracking Using Regression Networks) tracker - * - * GOTURN (@cite GOTURN) is kind of trackers based on Convolutional Neural Networks (CNN). While taking all advantages of CNN trackers, - * GOTURN is much faster due to offline training without online fine-tuning nature. - * GOTURN tracker addresses the problem of single target tracking: given a bounding box label of an object in the first frame of the video, - * we track that object through the rest of the video. NOTE: Current method of GOTURN does not handle occlusions; however, it is fairly - * robust to viewpoint changes, lighting changes, and deformations. - * Inputs of GOTURN are two RGB patches representing Target and Search patches resized to 227x227. - * Outputs of GOTURN are predicted bounding box coordinates, relative to Search patch coordinate system, in format X1,Y1,X2,Y2. - * Original paper is here: - * As long as original authors implementation: - * Implementation of training algorithm is placed in separately here due to 3d-party dependencies: - * - * GOTURN architecture goturn.prototxt and trained model goturn.caffemodel are accessible on opencv_extra GitHub repository. - */ -class CV_EXPORTS_W TrackerGOTURN : public Tracker -{ -protected: - TrackerGOTURN(); // use ::create() -public: - virtual ~TrackerGOTURN() CV_OVERRIDE; - - struct CV_EXPORTS_W_SIMPLE Params - { - CV_WRAP Params(); - CV_PROP_RW std::string modelTxt; - CV_PROP_RW std::string modelBin; - }; - - /** @brief Constructor - @param parameters GOTURN parameters TrackerGOTURN::Params - */ - static CV_WRAP - Ptr create(const TrackerGOTURN::Params& parameters = TrackerGOTURN::Params()); - - //void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; - //bool update(InputArray image, CV_OUT Rect& boundingBox) CV_OVERRIDE; -}; - //! @} #ifndef CV_DOXYGEN diff --git a/modules/tracking/include/opencv2/tracking/feature.hpp b/modules/tracking/include/opencv2/tracking/feature.hpp index ebc28ea944c..730afd0ebc4 100644 --- a/modules/tracking/include/opencv2/tracking/feature.hpp +++ b/modules/tracking/include/opencv2/tracking/feature.hpp @@ -60,7 +60,7 @@ inline namespace tracking { //! @addtogroup tracking_detail //! @{ -inline namespace feature { +inline namespace contrib_feature { #define FEATURES "features" diff --git a/modules/tracking/include/opencv2/tracking/onlineMIL.hpp b/modules/tracking/include/opencv2/tracking/onlineMIL.hpp deleted file mode 100644 index 9fb341eeba8..00000000000 --- a/modules/tracking/include/opencv2/tracking/onlineMIL.hpp +++ /dev/null @@ -1,119 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#ifndef __OPENCV_ONLINEMIL_HPP__ -#define __OPENCV_ONLINEMIL_HPP__ - -#include "opencv2/core.hpp" -#include - -namespace cv { -namespace detail { -inline namespace tracking { - -//! @addtogroup tracking_detail -//! @{ - -//TODO based on the original implementation -//http://vision.ucsd.edu/~bbabenko/project_miltrack.shtml - -class ClfOnlineStump; - -class CV_EXPORTS ClfMilBoost -{ - public: - struct CV_EXPORTS Params - { - Params(); - int _numSel; - int _numFeat; - float _lRate; - }; - - ClfMilBoost(); - ~ClfMilBoost(); - void init( const ClfMilBoost::Params ¶meters = ClfMilBoost::Params() ); - void update( const Mat& posx, const Mat& negx ); - std::vector classify( const Mat& x, bool logR = true ); - - inline float sigmoid( float x ) - { - return 1.0f / ( 1.0f + exp( -x ) ); - } - - private: - uint _numsamples; - ClfMilBoost::Params _myParams; - std::vector _selectors; - std::vector _weakclf; - uint _counter; - -}; - -class ClfOnlineStump -{ - public: - float _mu0, _mu1, _sig0, _sig1; - float _q; - int _s; - float _log_n1, _log_n0; - float _e1, _e0; - float _lRate; - - ClfOnlineStump(); - ClfOnlineStump( int ind ); - void init(); - void update( const Mat& posx, const Mat& negx, const cv::Mat_ & posw = cv::Mat_(), const cv::Mat_ & negw = cv::Mat_() ); - bool classify( const Mat& x, int i ); - float classifyF( const Mat& x, int i ); - std::vector classifySetF( const Mat& x ); - - private: - bool _trained; - int _ind; - -}; - -//! @} - -}}} // namespace - -#endif diff --git a/modules/tracking/include/opencv2/tracking/tracking_internals.hpp b/modules/tracking/include/opencv2/tracking/tracking_internals.hpp index fee43e3e086..92ca54a992a 100644 --- a/modules/tracking/include/opencv2/tracking/tracking_internals.hpp +++ b/modules/tracking/include/opencv2/tracking/tracking_internals.hpp @@ -1,43 +1,6 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. #ifndef OPENCV_TRACKING_DETAIL_HPP #define OPENCV_TRACKING_DETAIL_HPP @@ -52,11 +15,10 @@ * */ -#include "opencv2/core.hpp" +#include "opencv2/video/detail/tracking.private.hpp" #include "feature.hpp" // CvHaarEvaluator #include "onlineBoosting.hpp" // StrongClassifierDirectSelection -#include "onlineMIL.hpp" // ClfMilBoost namespace cv { namespace detail { @@ -77,13 +39,13 @@ These algorithms start from a bounding box of the target and with their internal avoid the drift during the tracking. These long-term trackers are able to evaluate online the quality of the location of the target in the new frame, without ground truth. -There are three main components: the TrackerSampler, the TrackerFeatureSet and the TrackerModel. The +There are three main components: the TrackerContribSampler, the TrackerContribFeatureSet and the TrackerModel. The first component is the object that computes the patches over the frame based on the last target -location. The TrackerFeatureSet is the class that manages the Features, is possible plug many kind +location. The TrackerContribFeatureSet is the class that manages the Features, is possible plug many kind of these (HAAR, HOG, LBP, Feature2D, etc). The last component is the internal representation of the target, it is the appearance model. It stores all state candidates and compute the trajectory (the most likely target states). The class TrackerTargetState represents a possible state of the target. -The TrackerSampler and the TrackerFeatureSet are the visual representation of the target, instead +The TrackerContribSampler and the TrackerContribFeatureSet are the visual representation of the target, instead the TrackerModel is the statistical model. A recent benchmark between these algorithms can be found in @cite OOT @@ -131,25 +93,25 @@ trackerMIL, trackerBoosting) -- we shall refer to this choice as to "classname" That function can (and probably will) return a pointer to some derived class of "classname", which will probably have a real constructor. -Every tracker has three component TrackerSampler, TrackerFeatureSet and TrackerModel. The first two +Every tracker has three component TrackerContribSampler, TrackerContribFeatureSet and TrackerModel. The first two are instantiated from Tracker base class, instead the last component is abstract, so you must implement your TrackerModel. -### TrackerSampler +### TrackerContribSampler -TrackerSampler is already instantiated, but you should define the sampling algorithm and add the -classes (or single class) to TrackerSampler. You can choose one of the ready implementation as -TrackerSamplerCSC or you can implement your sampling method, in this case the class must inherit -TrackerSamplerAlgorithm. Fill the samplingImpl method that writes the result in "sample" output +TrackerContribSampler is already instantiated, but you should define the sampling algorithm and add the +classes (or single class) to TrackerContribSampler. You can choose one of the ready implementation as +TrackerContribSamplerCSC or you can implement your sampling method, in this case the class must inherit +TrackerContribSamplerAlgorithm. Fill the samplingImpl method that writes the result in "sample" output argument. -Example of creating specialized TrackerSamplerAlgorithm TrackerSamplerCSC : : +Example of creating specialized TrackerContribSamplerAlgorithm TrackerContribSamplerCSC : : @code - class CV_EXPORTS_W TrackerSamplerCSC : public TrackerSamplerAlgorithm + class CV_EXPORTS_W TrackerContribSamplerCSC : public TrackerContribSamplerAlgorithm { public: - TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meters = TrackerSamplerCSC::Params() ); - ~TrackerSamplerCSC(); + TrackerContribSamplerCSC( const TrackerContribSamplerCSC::Params ¶meters = TrackerContribSamplerCSC::Params() ); + ~TrackerContribSamplerCSC(); ... protected: @@ -159,10 +121,10 @@ Example of creating specialized TrackerSamplerAlgorithm TrackerSamplerCSC : : }; @endcode -Example of adding TrackerSamplerAlgorithm to TrackerSampler : : +Example of adding TrackerContribSamplerAlgorithm to TrackerContribSampler : : @code - //sampler is the TrackerSampler - Ptr CSCSampler = new TrackerSamplerCSC( CSCparameters ); + //sampler is the TrackerContribSampler + Ptr CSCSampler = new TrackerContribSamplerCSC( CSCparameters ); if( !sampler->addTrackerSamplerAlgorithm( CSCSampler ) ) return false; @@ -170,23 +132,23 @@ Example of adding TrackerSamplerAlgorithm to TrackerSampler : : //sampler->addTrackerSamplerAlgorithm( "CSC" ); @endcode @sa - TrackerSamplerCSC, TrackerSamplerAlgorithm + TrackerContribSamplerCSC, TrackerContribSamplerAlgorithm -### TrackerFeatureSet +### TrackerContribFeatureSet -TrackerFeatureSet is already instantiated (as first) , but you should define what kinds of features +TrackerContribFeatureSet is already instantiated (as first) , but you should define what kinds of features you'll use in your tracker. You can use multiple feature types, so you can add a ready -implementation as TrackerFeatureHAAR in your TrackerFeatureSet or develop your own implementation. +implementation as TrackerContribFeatureHAAR in your TrackerContribFeatureSet or develop your own implementation. In this case, in the computeImpl method put the code that extract the features and in the selection method optionally put the code for the refinement and selection of the features. -Example of creating specialized TrackerFeature TrackerFeatureHAAR : : +Example of creating specialized TrackerFeature TrackerContribFeatureHAAR : : @code - class CV_EXPORTS_W TrackerFeatureHAAR : public TrackerFeature + class CV_EXPORTS_W TrackerContribFeatureHAAR : public TrackerFeature { public: - TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶meters = TrackerFeatureHAAR::Params() ); - ~TrackerFeatureHAAR(); + TrackerContribFeatureHAAR( const TrackerContribFeatureHAAR::Params ¶meters = TrackerContribFeatureHAAR::Params() ); + ~TrackerContribFeatureHAAR(); void selection( Mat& response, int npoints ); ... @@ -196,14 +158,14 @@ Example of creating specialized TrackerFeature TrackerFeatureHAAR : : }; @endcode -Example of adding TrackerFeature to TrackerFeatureSet : : +Example of adding TrackerFeature to TrackerContribFeatureSet : : @code - //featureSet is the TrackerFeatureSet - Ptr trackerFeature = new TrackerFeatureHAAR( HAARparameters ); + //featureSet is the TrackerContribFeatureSet + Ptr trackerFeature = new TrackerContribFeatureHAAR( HAARparameters ); featureSet->addTrackerFeature( trackerFeature ); @endcode @sa - TrackerFeatureHAAR, TrackerFeatureSet + TrackerContribFeatureHAAR, TrackerContribFeatureSet ### TrackerModel @@ -298,23 +260,17 @@ Example of creating specialized TrackerTargetState TrackerMILTargetState : : */ -/************************************ TrackerFeature Base Classes ************************************/ +/************************************ TrackerContribFeature Base Classes ************************************/ -/** @brief Abstract base class for TrackerFeature that represents the feature. +/** @brief Abstract base class for TrackerContribFeature that represents the feature. */ -class CV_EXPORTS TrackerFeature +class CV_EXPORTS TrackerContribFeature : public TrackerFeature { public: - virtual ~TrackerFeature(); + virtual ~TrackerContribFeature(); - /** @brief Compute the features in the images collection - @param images The images - @param response The output response - */ - void compute( const std::vector& images, Mat& response ); - - /** @brief Create TrackerFeature by tracker feature type - @param trackerFeatureType The TrackerFeature name + /** @brief Create TrackerContribFeature by tracker feature type + @param trackerFeatureType The TrackerContribFeature name The modes available now: @@ -325,25 +281,22 @@ class CV_EXPORTS TrackerFeature - "HOG" -- Histogram of Oriented Gradients features - "LBP" -- Local Binary Pattern features - "FEATURE2D" -- All types of Feature2D - */ - static Ptr create( const String& trackerFeatureType ); + */ + static Ptr create( const String& trackerFeatureType ); /** @brief Identify most effective features - @param response Collection of response for the specific TrackerFeature + @param response Collection of response for the specific TrackerContribFeature @param npoints Max number of features @note This method modifies the response parameter */ virtual void selection( Mat& response, int npoints ) = 0; - /** @brief Get the name of the specific TrackerFeature + /** @brief Get the name of the specific TrackerContribFeature */ String getClassName() const; protected: - - virtual bool computeImpl( const std::vector& images, Mat& response ) = 0; - String className; }; @@ -353,19 +306,19 @@ class CV_EXPORTS TrackerFeature See table I and section III C @cite AMVOT Appearance modelling -\> Visual representation (Table II, section 3.1 - 3.2) -TrackerFeatureSet is an aggregation of TrackerFeature +TrackerContribFeatureSet is an aggregation of TrackerContribFeature @sa - TrackerFeature + TrackerContribFeature */ -class CV_EXPORTS TrackerFeatureSet +class CV_EXPORTS TrackerContribFeatureSet { public: - TrackerFeatureSet(); + TrackerContribFeatureSet(); - ~TrackerFeatureSet(); + ~TrackerContribFeatureSet(); /** @brief Extract features from the images collection @param images The input images @@ -380,8 +333,8 @@ class CV_EXPORTS TrackerFeatureSet */ void removeOutliers(); - /** @brief Add TrackerFeature in the collection. Return true if TrackerFeature is added, false otherwise - @param trackerFeatureType The TrackerFeature name + /** @brief Add TrackerContribFeature in the collection. Return true if TrackerContribFeature is added, false otherwise + @param trackerFeatureType The TrackerContribFeature name The modes available now: @@ -393,32 +346,32 @@ class CV_EXPORTS TrackerFeatureSet - "LBP" -- Local Binary Pattern features - "FEATURE2D" -- All types of Feature2D - Example TrackerFeatureSet::addTrackerFeature : : + Example TrackerContribFeatureSet::addTrackerFeature : : @code //sample usage: - Ptr trackerFeature = new TrackerFeatureHAAR( HAARparameters ); + Ptr trackerFeature = ...; featureSet->addTrackerFeature( trackerFeature ); //or add CSC sampler with default parameters //featureSet->addTrackerFeature( "HAAR" ); @endcode - @note If you use the second method, you must initialize the TrackerFeature + @note If you use the second method, you must initialize the TrackerContribFeature */ bool addTrackerFeature( String trackerFeatureType ); /** @overload - @param feature The TrackerFeature class + @param feature The TrackerContribFeature class */ - bool addTrackerFeature( Ptr& feature ); + bool addTrackerFeature( Ptr& feature ); - /** @brief Get the TrackerFeature collection (TrackerFeature name, TrackerFeature pointer) + /** @brief Get the TrackerContribFeature collection (TrackerContribFeature name, TrackerContribFeature pointer) */ - const std::vector > >& getTrackerFeature() const; + const std::vector > >& getTrackerFeature() const; /** @brief Get the responses - @note Be sure to call extraction before getResponses Example TrackerFeatureSet::getResponses : : + @note Be sure to call extraction before getResponses Example TrackerContribFeatureSet::getResponses : : */ const std::vector& getResponses() const; @@ -427,25 +380,26 @@ class CV_EXPORTS TrackerFeatureSet void clearResponses(); bool blockAddTrackerFeature; - std::vector > > features; //list of features + std::vector > > features; //list of features std::vector responses; //list of response after compute }; -/************************************ TrackerSampler Base Classes ************************************/ -/** @brief Abstract base class for TrackerSamplerAlgorithm that represents the algorithm for the specific +/************************************ TrackerContribSampler Base Classes ************************************/ + +/** @brief Abstract base class for TrackerContribSamplerAlgorithm that represents the algorithm for the specific sampler. */ -class CV_EXPORTS TrackerSamplerAlgorithm +class CV_EXPORTS TrackerContribSamplerAlgorithm : public TrackerSamplerAlgorithm { public: /** * \brief Destructor */ - virtual ~TrackerSamplerAlgorithm(); + virtual ~TrackerContribSamplerAlgorithm(); - /** @brief Create TrackerSamplerAlgorithm by tracker sampler type. + /** @brief Create TrackerContribSamplerAlgorithm by tracker sampler type. @param trackerSamplerType The trackerSamplerType name The modes available now: @@ -453,7 +407,7 @@ class CV_EXPORTS TrackerSamplerAlgorithm - "CSC" -- Current State Center - "CS" -- Current State */ - static Ptr create( const String& trackerSamplerType ); + static Ptr create( const String& trackerSamplerType ); /** @brief Computes the regions starting from a position in an image. @@ -464,9 +418,9 @@ class CV_EXPORTS TrackerSamplerAlgorithm @param sample The computed samples @cite AAM Fig. 1 variable Sk */ - bool sampling( const Mat& image, Rect boundingBox, std::vector& sample ); + virtual bool sampling(const Mat& image, const Rect& boundingBox, std::vector& sample) CV_OVERRIDE; - /** @brief Get the name of the specific TrackerSamplerAlgorithm + /** @brief Get the name of the specific TrackerContribSamplerAlgorithm */ String getClassName() const; @@ -485,23 +439,23 @@ class CV_EXPORTS TrackerSamplerAlgorithm @cite AAM Sampling e Labeling. See table I and section III B -TrackerSampler is an aggregation of TrackerSamplerAlgorithm +TrackerContribSampler is an aggregation of TrackerContribSamplerAlgorithm @sa - TrackerSamplerAlgorithm + TrackerContribSamplerAlgorithm */ -class CV_EXPORTS TrackerSampler +class CV_EXPORTS TrackerContribSampler { public: /** * \brief Constructor */ - TrackerSampler(); + TrackerContribSampler(); /** * \brief Destructor */ - ~TrackerSampler(); + ~TrackerContribSampler(); /** @brief Computes the regions starting from a position in an image @param image The current frame @@ -509,26 +463,26 @@ class CV_EXPORTS TrackerSampler */ void sampling( const Mat& image, Rect boundingBox ); - /** @brief Return the collection of the TrackerSamplerAlgorithm + /** @brief Return the collection of the TrackerContribSamplerAlgorithm */ - const std::vector > >& getSamplers() const; + const std::vector > >& getSamplers() const; - /** @brief Return the samples from all TrackerSamplerAlgorithm, @cite AAM Fig. 1 variable Sk + /** @brief Return the samples from all TrackerContribSamplerAlgorithm, @cite AAM Fig. 1 variable Sk */ const std::vector& getSamples() const; - /** @brief Add TrackerSamplerAlgorithm in the collection. Return true if sampler is added, false otherwise - @param trackerSamplerAlgorithmType The TrackerSamplerAlgorithm name + /** @brief Add TrackerContribSamplerAlgorithm in the collection. Return true if sampler is added, false otherwise + @param trackerSamplerAlgorithmType The TrackerContribSamplerAlgorithm name The modes available now: - "CSC" -- Current State Center - "CS" -- Current State - "PF" -- Particle Filtering - Example TrackerSamplerAlgorithm::addTrackerSamplerAlgorithm : : + Example TrackerContribSamplerAlgorithm::addTrackerContribSamplerAlgorithm : : @code - TrackerSamplerCSC::Params CSCparameters; - Ptr CSCSampler = new TrackerSamplerCSC( CSCparameters ); + TrackerContribSamplerCSC::Params CSCparameters; + Ptr CSCSampler = new TrackerContribSamplerCSC( CSCparameters ); if( !sampler->addTrackerSamplerAlgorithm( CSCSampler ) ) return false; @@ -536,300 +490,23 @@ class CV_EXPORTS TrackerSampler //or add CSC sampler with default parameters //sampler->addTrackerSamplerAlgorithm( "CSC" ); @endcode - @note If you use the second method, you must initialize the TrackerSamplerAlgorithm + @note If you use the second method, you must initialize the TrackerContribSamplerAlgorithm */ bool addTrackerSamplerAlgorithm( String trackerSamplerAlgorithmType ); /** @overload - @param sampler The TrackerSamplerAlgorithm + @param sampler The TrackerContribSamplerAlgorithm */ - bool addTrackerSamplerAlgorithm( Ptr& sampler ); + bool addTrackerSamplerAlgorithm( Ptr& sampler ); private: - std::vector > > samplers; + std::vector > > samplers; std::vector samples; bool blockAddTrackerSampler; void clearSamples(); }; -/************************************ TrackerModel Base Classes ************************************/ - -/** @brief Abstract base class for TrackerTargetState that represents a possible state of the target. - -See @cite AAM \f$\hat{x}^{i}_{k}\f$ all the states candidates. - -Inherits this class with your Target state, In own implementation you can add scale variation, -width, height, orientation, etc. - */ -class CV_EXPORTS TrackerTargetState -{ - public: - virtual ~TrackerTargetState() - { - } - ; - /** - * \brief Get the position - * \return The position - */ - Point2f getTargetPosition() const; - - /** - * \brief Set the position - * \param position The position - */ - void setTargetPosition( const Point2f& position ); - /** - * \brief Get the width of the target - * \return The width of the target - */ - int getTargetWidth() const; - - /** - * \brief Set the width of the target - * \param width The width of the target - */ - void setTargetWidth( int width ); - /** - * \brief Get the height of the target - * \return The height of the target - */ - int getTargetHeight() const; - - /** - * \brief Set the height of the target - * \param height The height of the target - */ - void setTargetHeight( int height ); - - protected: - Point2f targetPosition; - int targetWidth; - int targetHeight; - -}; - -/** @brief Represents the model of the target at frame \f$k\f$ (all states and scores) - -See @cite AAM The set of the pair \f$\langle \hat{x}^{i}_{k}, C^{i}_{k} \rangle\f$ -@sa TrackerTargetState - */ -typedef std::vector, float> > ConfidenceMap; - -/** @brief Represents the estimate states for all frames - -@cite AAM \f$x_{k}\f$ is the trajectory of the target up to time \f$k\f$ - -@sa TrackerTargetState - */ -typedef std::vector > Trajectory; - -/** @brief Abstract base class for TrackerStateEstimator that estimates the most likely target state. - -See @cite AAM State estimator - -See @cite AMVOT Statistical modeling (Fig. 3), Table III (generative) - IV (discriminative) - V (hybrid) - */ -class CV_EXPORTS TrackerStateEstimator -{ - public: - virtual ~TrackerStateEstimator(); - - /** @brief Estimate the most likely target state, return the estimated state - @param confidenceMaps The overall appearance model as a list of :cConfidenceMap - */ - Ptr estimate( const std::vector& confidenceMaps ); - - /** @brief Update the ConfidenceMap with the scores - @param confidenceMaps The overall appearance model as a list of :cConfidenceMap - */ - void update( std::vector& confidenceMaps ); - - /** @brief Create TrackerStateEstimator by tracker state estimator type - @param trackeStateEstimatorType The TrackerStateEstimator name - - The modes available now: - - - "BOOSTING" -- Boosting-based discriminative appearance models. See @cite AMVOT section 4.4 - - The modes available soon: - - - "SVM" -- SVM-based discriminative appearance models. See @cite AMVOT section 4.5 - */ - static Ptr create( const String& trackeStateEstimatorType ); - - /** @brief Get the name of the specific TrackerStateEstimator - */ - String getClassName() const; - - protected: - - virtual Ptr estimateImpl( const std::vector& confidenceMaps ) = 0; - virtual void updateImpl( std::vector& confidenceMaps ) = 0; - String className; -}; - -/** @brief Abstract class that represents the model of the target. It must be instantiated by specialized -tracker - -See @cite AAM Ak - -Inherits this with your TrackerModel - */ -class CV_EXPORTS TrackerModel -{ - public: - - /** - * \brief Constructor - */ - TrackerModel(); - - /** - * \brief Destructor - */ - virtual ~TrackerModel(); - - /** @brief Set TrackerEstimator, return true if the tracker state estimator is added, false otherwise - @param trackerStateEstimator The TrackerStateEstimator - @note You can add only one TrackerStateEstimator - */ - bool setTrackerStateEstimator( Ptr trackerStateEstimator ); - - /** @brief Estimate the most likely target location - - @cite AAM ME, Model Estimation table I - @param responses Features extracted from TrackerFeatureSet - */ - void modelEstimation( const std::vector& responses ); - - /** @brief Update the model - - @cite AAM MU, Model Update table I - */ - void modelUpdate(); - - /** @brief Run the TrackerStateEstimator, return true if is possible to estimate a new state, false otherwise - */ - bool runStateEstimator(); - - /** @brief Set the current TrackerTargetState in the Trajectory - @param lastTargetState The current TrackerTargetState - */ - void setLastTargetState( const Ptr& lastTargetState ); - - /** @brief Get the last TrackerTargetState from Trajectory - */ - Ptr getLastTargetState() const; - - /** @brief Get the list of the ConfidenceMap - */ - const std::vector& getConfidenceMaps() const; - - /** @brief Get the last ConfidenceMap for the current frame - */ - const ConfidenceMap& getLastConfidenceMap() const; - - /** @brief Get the TrackerStateEstimator - */ - Ptr getTrackerStateEstimator() const; - - private: - - void clearCurrentConfidenceMap(); - - protected: - std::vector confidenceMaps; - Ptr stateEstimator; - ConfidenceMap currentConfidenceMap; - Trajectory trajectory; - int maxCMLength; - - virtual void modelEstimationImpl( const std::vector& responses ) = 0; - virtual void modelUpdateImpl() = 0; - -}; - - -/************************************ Specific TrackerStateEstimator Classes ************************************/ - -/** @brief TrackerStateEstimator based on Boosting - */ -class CV_EXPORTS TrackerStateEstimatorMILBoosting : public TrackerStateEstimator -{ - public: - - /** - * Implementation of the target state for TrackerStateEstimatorMILBoosting - */ - class TrackerMILTargetState : public TrackerTargetState - { - - public: - /** - * \brief Constructor - * \param position Top left corner of the bounding box - * \param width Width of the bounding box - * \param height Height of the bounding box - * \param foreground label for target or background - * \param features features extracted - */ - TrackerMILTargetState( const Point2f& position, int width, int height, bool foreground, const Mat& features ); - - /** - * \brief Destructor - */ - ~TrackerMILTargetState() - { - } - ; - - /** @brief Set label: true for target foreground, false for background - @param foreground Label for background/foreground - */ - void setTargetFg( bool foreground ); - /** @brief Set the features extracted from TrackerFeatureSet - @param features The features extracted - */ - void setFeatures( const Mat& features ); - /** @brief Get the label. Return true for target foreground, false for background - */ - bool isTargetFg() const; - /** @brief Get the features extracted - */ - Mat getFeatures() const; - - private: - bool isTarget; - Mat targetFeatures; - }; - - /** @brief Constructor - @param nFeatures Number of features for each sample - */ - TrackerStateEstimatorMILBoosting( int nFeatures = 250 ); - ~TrackerStateEstimatorMILBoosting(); - - /** @brief Set the current confidenceMap - @param confidenceMap The current :cConfidenceMap - */ - void setCurrentConfidenceMap( ConfidenceMap& confidenceMap ); - - protected: - Ptr estimateImpl( const std::vector& confidenceMaps ) CV_OVERRIDE; - void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; - - private: - uint max_idx( const std::vector &v ); - void prepareData( const ConfidenceMap& confidenceMap, Mat& positive, Mat& negative ); - - ClfMilBoost boostMILModel; - bool trained; - int numFeatures; - - ConfidenceMap currentConfidenceMap; -}; /** @brief TrackerStateEstimatorAdaBoosting based on ADA-Boosting */ @@ -838,7 +515,7 @@ class CV_EXPORTS TrackerStateEstimatorAdaBoosting : public TrackerStateEstimator public: /** @brief Implementation of the target state for TrackerAdaBoostingTargetState */ - class TrackerAdaBoostingTargetState : public TrackerTargetState + class CV_EXPORTS TrackerAdaBoostingTargetState : public TrackerTargetState { public: @@ -860,7 +537,7 @@ class CV_EXPORTS TrackerStateEstimatorAdaBoosting : public TrackerStateEstimator } ; - /** @brief Set the features extracted from TrackerFeatureSet + /** @brief Set the features extracted from TrackerContribFeatureSet @param responses The features extracted */ void setTargetResponses( const Mat& responses ); @@ -940,6 +617,7 @@ class CV_EXPORTS TrackerStateEstimatorAdaBoosting : public TrackerStateEstimator ConfidenceMap currentConfidenceMap; }; + /** * \brief TrackerStateEstimator based on SVM */ @@ -954,11 +632,13 @@ class CV_EXPORTS TrackerStateEstimatorSVM : public TrackerStateEstimator void updateImpl( std::vector& confidenceMaps ) CV_OVERRIDE; }; + + /************************************ Specific TrackerSamplerAlgorithm Classes ************************************/ /** @brief TrackerSampler based on CSC (current state centered), used by MIL algorithm TrackerMIL */ -class CV_EXPORTS TrackerSamplerCSC : public TrackerSamplerAlgorithm +class CV_EXPORTS TrackerContribSamplerCSC : public TrackerContribSamplerAlgorithm { public: enum @@ -982,11 +662,11 @@ class CV_EXPORTS TrackerSamplerCSC : public TrackerSamplerAlgorithm }; /** @brief Constructor - @param parameters TrackerSamplerCSC parameters TrackerSamplerCSC::Params + @param parameters TrackerContribSamplerCSC parameters TrackerContribSamplerCSC::Params */ - TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meters = TrackerSamplerCSC::Params() ); + TrackerContribSamplerCSC( const TrackerContribSamplerCSC::Params ¶meters = TrackerContribSamplerCSC::Params() ); - /** @brief Set the sampling mode of TrackerSamplerCSC + /** @brief Set the sampling mode of TrackerContribSamplerCSC @param samplingMode The sampling mode The modes are: @@ -999,11 +679,11 @@ class CV_EXPORTS TrackerSamplerCSC : public TrackerSamplerAlgorithm */ void setMode( int samplingMode ); - ~TrackerSamplerCSC(); + ~TrackerContribSamplerCSC(); protected: - bool samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) CV_OVERRIDE; + bool samplingImpl(const Mat& image, Rect boundingBox, std::vector& sample) CV_OVERRIDE; private: @@ -1014,9 +694,10 @@ class CV_EXPORTS TrackerSamplerCSC : public TrackerSamplerAlgorithm std::vector sampleImage( const Mat& img, int x, int y, int w, int h, float inrad, float outrad = 0, int maxnum = 1000000 ); }; -/** @brief TrackerSampler based on CS (current state), used by algorithm TrackerBoosting + +/** @brief TrackerContribSampler based on CS (current state), used by algorithm TrackerBoosting */ -class CV_EXPORTS TrackerSamplerCS : public TrackerSamplerAlgorithm +class CV_EXPORTS TrackerSamplerCS : public TrackerContribSamplerAlgorithm { public: enum @@ -1083,7 +764,7 @@ It should be noted, that the definition of "similarity" between two rectangles i their histograms. As experiments show, tracker is *not* very succesfull if target is assumed to strongly change its dimensions. */ -class CV_EXPORTS TrackerSamplerPF : public TrackerSamplerAlgorithm +class CV_EXPORTS TrackerSamplerPF : public TrackerContribSamplerAlgorithm { public: /** @brief This structure contains all the parameters that can be varied during the course of sampling @@ -1113,12 +794,14 @@ class CV_EXPORTS TrackerSamplerPF : public TrackerSamplerAlgorithm Ptr _function; }; -/************************************ Specific TrackerFeature Classes ************************************/ + + +/************************************ Specific TrackerContribFeature Classes ************************************/ /** - * \brief TrackerFeature based on Feature2D + * \brief TrackerContribFeature based on Feature2D */ -class CV_EXPORTS TrackerFeatureFeature2d : public TrackerFeature +class CV_EXPORTS TrackerFeatureFeature2d : public TrackerContribFeature { public: @@ -1143,9 +826,9 @@ class CV_EXPORTS TrackerFeatureFeature2d : public TrackerFeature }; /** - * \brief TrackerFeature based on HOG + * \brief TrackerContribFeature based on HOG */ -class CV_EXPORTS TrackerFeatureHOG : public TrackerFeature +class CV_EXPORTS TrackerFeatureHOG : public TrackerContribFeature { public: @@ -1161,10 +844,10 @@ class CV_EXPORTS TrackerFeatureHOG : public TrackerFeature }; -/** @brief TrackerFeature based on HAAR features, used by TrackerMIL and many others algorithms +/** @brief TrackerContribFeature based on HAAR features, used by TrackerMIL and many others algorithms @note HAAR features implementation is copied from apps/traincascade and modified according to MIL */ -class CV_EXPORTS TrackerFeatureHAAR : public TrackerFeature +class CV_EXPORTS TrackerContribFeatureHAAR : public TrackerContribFeature { public: struct CV_EXPORTS Params @@ -1176,21 +859,21 @@ class CV_EXPORTS TrackerFeatureHAAR : public TrackerFeature }; /** @brief Constructor - @param parameters TrackerFeatureHAAR parameters TrackerFeatureHAAR::Params + @param parameters TrackerContribFeatureHAAR parameters TrackerContribFeatureHAAR::Params */ - TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶meters = TrackerFeatureHAAR::Params() ); + TrackerContribFeatureHAAR( const TrackerContribFeatureHAAR::Params ¶meters = TrackerContribFeatureHAAR::Params() ); - ~TrackerFeatureHAAR() CV_OVERRIDE; + ~TrackerContribFeatureHAAR() CV_OVERRIDE; /** @brief Compute the features only for the selected indices in the images collection @param selFeatures indices of selected features @param images The images - @param response Collection of response for the specific TrackerFeature + @param response Collection of response for the specific TrackerContribFeature */ bool extractSelected( const std::vector selFeatures, const std::vector& images, Mat& response ); /** @brief Identify most effective features - @param response Collection of response for the specific TrackerFeature + @param response Collection of response for the specific TrackerContribFeature @param npoints Max number of features @note This method modifies the response parameter @@ -1224,9 +907,9 @@ class CV_EXPORTS TrackerFeatureHAAR : public TrackerFeature }; /** - * \brief TrackerFeature based on LBP + * \brief TrackerContribFeature based on LBP */ -class CV_EXPORTS TrackerFeatureLBP : public TrackerFeature +class CV_EXPORTS TrackerFeatureLBP : public TrackerContribFeature { public: diff --git a/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp b/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp index bf0bef33c23..e0f17064c62 100644 --- a/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp +++ b/modules/tracking/include/opencv2/tracking/tracking_legacy.hpp @@ -104,8 +104,8 @@ class CV_EXPORTS_W Tracker : public virtual Algorithm bool isInit; - Ptr featureSet; - Ptr sampler; + Ptr featureSet; + Ptr sampler; Ptr model; }; @@ -123,7 +123,7 @@ Original code can be found here upgradeTrackingAPI(const Ptr& legacy_tracker); +CV_EXPORTS_W Ptr upgradeTrackingAPI(const Ptr& legacy_tracker); //! @} diff --git a/modules/tracking/misc/java/test/TrackerCreateTest.java b/modules/tracking/misc/java/test/TrackerCreateTest.java index 189ccfcd5e7..f4db6abadad 100644 --- a/modules/tracking/misc/java/test/TrackerCreateTest.java +++ b/modules/tracking/misc/java/test/TrackerCreateTest.java @@ -4,11 +4,10 @@ import org.opencv.core.CvException; import org.opencv.test.OpenCVTestCase; -import org.opencv.tracking.Tracking; -import org.opencv.tracking.Tracker; -import org.opencv.tracking.TrackerGOTURN; +import org.opencv.video.Tracker; +import org.opencv.video.TrackerGOTURN; import org.opencv.tracking.TrackerKCF; -import org.opencv.tracking.TrackerMIL; +import org.opencv.video.TrackerMIL; public class TrackerCreateTest extends OpenCVTestCase { diff --git a/modules/tracking/misc/python/pyopencv_tracking.hpp b/modules/tracking/misc/python/pyopencv_tracking.hpp index dd4d5269628..10c4eeb9da2 100644 --- a/modules/tracking/misc/python/pyopencv_tracking.hpp +++ b/modules/tracking/misc/python/pyopencv_tracking.hpp @@ -1,6 +1,4 @@ #ifdef HAVE_OPENCV_TRACKING typedef TrackerCSRT::Params TrackerCSRT_Params; typedef TrackerKCF::Params TrackerKCF_Params; -typedef TrackerMIL::Params TrackerMIL_Params; -typedef TrackerGOTURN::Params TrackerGOTURN_Params; #endif diff --git a/modules/tracking/perf/perf_trackers.cpp b/modules/tracking/perf/perf_trackers.cpp index e76b15b2c02..bd47b750fb0 100644 --- a/modules/tracking/perf/perf_trackers.cpp +++ b/modules/tracking/perf/perf_trackers.cpp @@ -87,12 +87,6 @@ void Tracking::runTrackingTest(const Ptr& tracker, const TrackingParams //================================================================================================== -PERF_TEST_P(Tracking, MIL, testing::ValuesIn(getTrackingParams())) -{ - auto tracker = TrackerMIL::create(); - runTrackingTest(tracker, GetParam()); -} - PERF_TEST_P(Tracking, Boosting, testing::ValuesIn(getTrackingParams())) { auto tracker = legacy::TrackerBoosting::create(); @@ -105,15 +99,4 @@ PERF_TEST_P(Tracking, TLD, testing::ValuesIn(getTrackingParams())) runTrackingTest(tracker, GetParam()); } -PERF_TEST_P(Tracking, GOTURN, testing::ValuesIn(getTrackingParams())) -{ - std::string model = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.prototxt"); - std::string weights = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.caffemodel", false); - TrackerGOTURN::Params params; - params.modelTxt = model; - params.modelBin = weights; - auto tracker = TrackerGOTURN::create(params); - runTrackingTest(tracker, GetParam()); -} - }} // namespace diff --git a/modules/tracking/src/gtrTracker.cpp b/modules/tracking/src/gtrTracker.cpp deleted file mode 100644 index b6dc1e004f7..00000000000 --- a/modules/tracking/src/gtrTracker.cpp +++ /dev/null @@ -1,177 +0,0 @@ -/*/////////////////////////////////////////////////////////////////////////////////////// -// -// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. -// -// By downloading, copying, installing or using the software you agree to this license. -// If you do not agree to this license, do not download, install, -// copy or use the software. -// -// -// License Agreement -// For Open Source Computer Vision Library -// -// Copyright (C) 2013, OpenCV Foundation, all rights reserved. -// Third party copyrights are property of their respective owners. -// -// Redistribution and use in source and binary forms, with or without modification, -// are permitted provided that the following conditions are met: -// -// * Redistribution's of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Redistribution's in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * The name of the copyright holders may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// This software is provided by the copyright holders and contributors "as is" and -// any express or implied warranties, including, but not limited to, the implied -// warranties of merchantability and fitness for a particular purpose are disclaimed. -// In no event shall the Intel Corporation or contributors be liable for any direct, -// indirect, incidental, special, exemplary, or consequential damages -// (including, but not limited to, procurement of substitute goods or services; -// loss of use, data, or profits; or business interruption) however caused -// and on any theory of liability, whether in contract, strict liability, -// or tort (including negligence or otherwise) arising in any way out of -// the use of this software, even if advised of the possibility of such damage. -// -//M*/ -#include "precomp.hpp" - -#ifdef HAVE_OPENCV_DNN -#include "opencv2/dnn.hpp" -#endif - -namespace cv { -inline namespace tracking { - -TrackerGOTURN::TrackerGOTURN() -{ - // nothing -} - -TrackerGOTURN::~TrackerGOTURN() -{ - // nothing -} - -TrackerGOTURN::Params::Params() -{ - modelTxt = "goturn.prototxt"; - modelBin = "goturn.caffemodel"; -} - -#ifdef HAVE_OPENCV_DNN - -class TrackerGOTURNImpl : public TrackerGOTURN -{ -public: - TrackerGOTURNImpl(const TrackerGOTURN::Params ¶meters) - : params(parameters) - { - // Load GOTURN architecture from *.prototxt and pretrained weights from *.caffemodel - net = dnn::readNetFromCaffe(params.modelTxt, params.modelBin); - CV_Assert(!net.empty()); - } - - void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; - bool update(InputArray image, Rect& boundingBox) CV_OVERRIDE; - - void setBoudingBox(Rect boundingBox) - { - if (image_.empty()) - CV_Error(Error::StsInternal, "Set image first"); - boundingBox_ = boundingBox & Rect(Point(0, 0), image_.size()); - } - - TrackerGOTURN::Params params; - - dnn::Net net; - Rect boundingBox_; - Mat image_; -}; - -void TrackerGOTURNImpl::init(InputArray image, const Rect& boundingBox) -{ - image_ = image.getMat().clone(); - setBoudingBox(boundingBox); -} - -bool TrackerGOTURNImpl::update(InputArray image, Rect& boundingBox) -{ - int INPUT_SIZE = 227; - //Using prevFrame & prevBB from model and curFrame GOTURN calculating curBB - InputArray curFrame = image; - Mat prevFrame = image_; - Rect2d prevBB = boundingBox_; - Rect curBB; - - float padTargetPatch = 2.0; - Rect2f searchPatchRect, targetPatchRect; - Point2f currCenter, prevCenter; - Mat prevFramePadded, curFramePadded; - Mat searchPatch, targetPatch; - - prevCenter.x = (float)(prevBB.x + prevBB.width / 2); - prevCenter.y = (float)(prevBB.y + prevBB.height / 2); - - targetPatchRect.width = (float)(prevBB.width*padTargetPatch); - targetPatchRect.height = (float)(prevBB.height*padTargetPatch); - targetPatchRect.x = (float)(prevCenter.x - prevBB.width*padTargetPatch / 2.0 + targetPatchRect.width); - targetPatchRect.y = (float)(prevCenter.y - prevBB.height*padTargetPatch / 2.0 + targetPatchRect.height); - - targetPatchRect.width = std::min(targetPatchRect.width, (float)prevFrame.cols); - targetPatchRect.height = std::min(targetPatchRect.height, (float)prevFrame.rows); - targetPatchRect.x = std::max(-prevFrame.cols * 0.5f, std::min(targetPatchRect.x, prevFrame.cols * 1.5f)); - targetPatchRect.y = std::max(-prevFrame.rows * 0.5f, std::min(targetPatchRect.y, prevFrame.rows * 1.5f)); - - copyMakeBorder(prevFrame, prevFramePadded, (int)targetPatchRect.height, (int)targetPatchRect.height, (int)targetPatchRect.width, (int)targetPatchRect.width, BORDER_REPLICATE); - targetPatch = prevFramePadded(targetPatchRect).clone(); - - copyMakeBorder(curFrame, curFramePadded, (int)targetPatchRect.height, (int)targetPatchRect.height, (int)targetPatchRect.width, (int)targetPatchRect.width, BORDER_REPLICATE); - searchPatch = curFramePadded(targetPatchRect).clone(); - - // Preprocess - // Resize - resize(targetPatch, targetPatch, Size(INPUT_SIZE, INPUT_SIZE), 0, 0, INTER_LINEAR_EXACT); - resize(searchPatch, searchPatch, Size(INPUT_SIZE, INPUT_SIZE), 0, 0, INTER_LINEAR_EXACT); - - // Convert to Float type and subtract mean - Mat targetBlob = dnn::blobFromImage(targetPatch, 1.0f, Size(), Scalar::all(128), false); - Mat searchBlob = dnn::blobFromImage(searchPatch, 1.0f, Size(), Scalar::all(128), false); - - net.setInput(targetBlob, "data1"); - net.setInput(searchBlob, "data2"); - - Mat resMat = net.forward("scale").reshape(1, 1); - - curBB.x = cvRound(targetPatchRect.x + (resMat.at(0) * targetPatchRect.width / INPUT_SIZE) - targetPatchRect.width); - curBB.y = cvRound(targetPatchRect.y + (resMat.at(1) * targetPatchRect.height / INPUT_SIZE) - targetPatchRect.height); - curBB.width = cvRound((resMat.at(2) - resMat.at(0)) * targetPatchRect.width / INPUT_SIZE); - curBB.height = cvRound((resMat.at(3) - resMat.at(1)) * targetPatchRect.height / INPUT_SIZE); - - // Predicted BB - boundingBox = curBB & Rect(Point(0, 0), image_.size()); - - // Set new model image and BB from current frame - image_ = image.getMat().clone(); - setBoudingBox(curBB); - return true; -} - -Ptr TrackerGOTURN::create(const TrackerGOTURN::Params& parameters) -{ - return makePtr(parameters); -} - -#else // OPENCV_HAVE_DNN -Ptr TrackerGOTURN::create(const TrackerGOTURN::Params& parameters) -{ - (void)(parameters); - CV_Error(cv::Error::StsNotImplemented, "to use GOTURN, the tracking module needs to be built with opencv_dnn !"); -} -#endif // OPENCV_HAVE_DNN - -}} // namespace diff --git a/modules/tracking/src/legacy/tracker.legacy.hpp b/modules/tracking/src/legacy/tracker.legacy.hpp index 788a758bcd2..2622ffbba32 100644 --- a/modules/tracking/src/legacy/tracker.legacy.hpp +++ b/modules/tracking/src/legacy/tracker.legacy.hpp @@ -65,19 +65,13 @@ bool Tracker::init( InputArray image, const Rect2d& boundingBox ) if( image.empty() ) return false; - sampler = Ptr( new TrackerSampler() ); - featureSet = Ptr( new TrackerFeatureSet() ); + sampler = Ptr( new TrackerContribSampler() ); + featureSet = Ptr( new TrackerContribFeatureSet() ); model = Ptr(); bool initTracker = initImpl( image.getMat(), boundingBox ); - //check if the model component is initialized - if (!model) - { - CV_Error( -1, "The model is not initialized" ); - } - - if( initTracker ) + if (initTracker) { isInit = true; } @@ -101,7 +95,7 @@ bool Tracker::update( InputArray image, Rect2d& boundingBox ) -class LegacyTrackerWrapper : public cv::tracking::Tracker +class LegacyTrackerWrapper : public cv::Tracker { const Ptr legacy_tracker_; public: @@ -132,7 +126,7 @@ class LegacyTrackerWrapper : public cv::tracking::Tracker }; -CV_EXPORTS_W Ptr upgradeTrackingAPI(const Ptr& legacy_tracker) +CV_EXPORTS_W Ptr upgradeTrackingAPI(const Ptr& legacy_tracker) { return makePtr(legacy_tracker); } diff --git a/modules/tracking/src/legacy/trackerCSRT.legacy.hpp b/modules/tracking/src/legacy/trackerCSRT.legacy.hpp index b332ef25c08..c42d270bd8b 100644 --- a/modules/tracking/src/legacy/trackerCSRT.legacy.hpp +++ b/modules/tracking/src/legacy/trackerCSRT.legacy.hpp @@ -33,8 +33,8 @@ class TrackerCSRTImpl CV_FINAL : public legacy::TrackerCSRT { impl.init(image, boundingBox); model = impl.model; - sampler = makePtr(); - featureSet = makePtr(); + sampler = makePtr(); + featureSet = makePtr(); isInit = true; return true; } diff --git a/modules/tracking/src/legacy/trackerKCF.legacy.hpp b/modules/tracking/src/legacy/trackerKCF.legacy.hpp index 86e895ec284..e10284411e9 100644 --- a/modules/tracking/src/legacy/trackerKCF.legacy.hpp +++ b/modules/tracking/src/legacy/trackerKCF.legacy.hpp @@ -72,8 +72,8 @@ class TrackerKCFImpl CV_FINAL : public legacy::TrackerKCF { impl.init(image, boundingBox); model = impl.model; - sampler = makePtr(); - featureSet = makePtr(); + sampler = makePtr(); + featureSet = makePtr(); isInit = true; return true; } diff --git a/modules/tracking/src/onlineMIL.cpp b/modules/tracking/src/onlineMIL.cpp deleted file mode 100644 index 29fa9fe3499..00000000000 --- a/modules/tracking/src/onlineMIL.cpp +++ /dev/null @@ -1,381 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#include "precomp.hpp" -#include "opencv2/tracking/onlineMIL.hpp" - -#define sign(s) ((s > 0 ) ? 1 : ((s<0) ? -1 : 0)) - -template class SortableElementRev -{ - public: - T _val; - int _ind; - SortableElementRev() : - _ind( 0 ) - { - } - SortableElementRev( T val, int ind ) - { - _val = val; - _ind = ind; - } - bool operator<( SortableElementRev &b ) - { - return ( _val < b._val ); - } - ; -}; - -static bool CompareSortableElementRev( const SortableElementRev& i, const SortableElementRev& j ) -{ - return i._val < j._val; -} - -template void sort_order_des( std::vector &v, std::vector &order ) -{ - uint n = (uint) v.size(); - std::vector > v2; - v2.resize( n ); - order.clear(); - order.resize( n ); - for ( uint i = 0; i < n; i++ ) - { - v2[i]._ind = i; - v2[i]._val = v[i]; - } - //std::sort( v2.begin(), v2.end() ); - std::sort( v2.begin(), v2.end(), CompareSortableElementRev ); - for ( uint i = 0; i < n; i++ ) - { - order[i] = v2[i]._ind; - v[i] = v2[i]._val; - } -} -; - -namespace cv -{ - -//implementations for strong classifier - -ClfMilBoost::Params::Params() -{ - _numSel = 50; - _numFeat = 250; - _lRate = 0.85f; -} - -ClfMilBoost::ClfMilBoost() -{ - _myParams = ClfMilBoost::Params(); - _numsamples = 0; -} - -ClfMilBoost::~ClfMilBoost() -{ - _selectors.clear(); - for ( size_t i = 0; i < _weakclf.size(); i++ ) - delete _weakclf.at( i ); -} - -void ClfMilBoost::init( const ClfMilBoost::Params ¶meters ) -{ - _myParams = parameters; - _numsamples = 0; - - //_ftrs = Ftr::generate( _myParams->_ftrParams, _myParams->_numFeat ); - // if( params->_storeFtrHistory ) - // Ftr::toViz( _ftrs, "haarftrs" ); - _weakclf.resize( _myParams._numFeat ); - for ( int k = 0; k < _myParams._numFeat; k++ ) - { - _weakclf[k] = new ClfOnlineStump( k ); - _weakclf[k]->_lRate = _myParams._lRate; - - } - _counter = 0; -} - -void ClfMilBoost::update( const Mat& posx, const Mat& negx ) -{ - int numneg = negx.rows; - int numpos = posx.rows; - - // compute ftrs - //if( !posx.ftrsComputed() ) - // Ftr::compute( posx, _ftrs ); - //if( !negx.ftrsComputed() ) - // Ftr::compute( negx, _ftrs ); - - // initialize H - static std::vector Hpos, Hneg; - Hpos.clear(); - Hneg.clear(); - Hpos.resize( posx.rows, 0.0f ), Hneg.resize( negx.rows, 0.0f ); - - _selectors.clear(); - std::vector posw( posx.rows ), negw( negx.rows ); - std::vector > pospred( _weakclf.size() ), negpred( _weakclf.size() ); - - // train all weak classifiers without weights -#ifdef _OPENMP -#pragma omp parallel for -#endif - for ( int m = 0; m < _myParams._numFeat; m++ ) - { - _weakclf[m]->update( posx, negx ); - pospred[m] = _weakclf[m]->classifySetF( posx ); - negpred[m] = _weakclf[m]->classifySetF( negx ); - } - - // pick the best features - for ( int s = 0; s < _myParams._numSel; s++ ) - { - - // compute errors/likl for all weak clfs - std::vector poslikl( _weakclf.size(), 1.0f ), neglikl( _weakclf.size() ), likl( _weakclf.size() ); -#ifdef _OPENMP -#pragma omp parallel for -#endif - for ( int w = 0; w < (int) _weakclf.size(); w++ ) - { - float lll = 1.0f; - for ( int j = 0; j < numpos; j++ ) - lll *= ( 1 - sigmoid( Hpos[j] + pospred[w][j] ) ); - poslikl[w] = (float) -log( 1 - lll + 1e-5 ); - - lll = 0.0f; - for ( int j = 0; j < numneg; j++ ) - lll += (float) -log( 1e-5f + 1 - sigmoid( Hneg[j] + negpred[w][j] ) ); - neglikl[w] = lll; - - likl[w] = poslikl[w] / numpos + neglikl[w] / numneg; - } - - // pick best weak clf - std::vector order; - sort_order_des( likl, order ); - - // find best weakclf that isn't already included - for ( uint k = 0; k < order.size(); k++ ) - if( std::count( _selectors.begin(), _selectors.end(), order[k] ) == 0 ) - { - _selectors.push_back( order[k] ); - break; - } - - // update H = H + h_m -#ifdef _OPENMP -#pragma omp parallel for -#endif - for ( int k = 0; k < posx.rows; k++ ) - Hpos[k] += pospred[_selectors[s]][k]; -#ifdef _OPENMP -#pragma omp parallel for -#endif - for ( int k = 0; k < negx.rows; k++ ) - Hneg[k] += negpred[_selectors[s]][k]; - - } - - //if( _myParams->_storeFtrHistory ) - //for ( uint j = 0; j < _selectors.size(); j++ ) - // _ftrHist( _selectors[j], _counter ) = 1.0f / ( j + 1 ); - - _counter++; - /* */ - return; -} - -std::vector ClfMilBoost::classify( const Mat& x, bool logR ) -{ - int numsamples = x.rows; - std::vector res( numsamples ); - std::vector tr; - - for ( uint w = 0; w < _selectors.size(); w++ ) - { - tr = _weakclf[_selectors[w]]->classifySetF( x ); -#ifdef _OPENMP -#pragma omp parallel for -#endif - for ( int j = 0; j < numsamples; j++ ) - { - res[j] += tr[j]; - } - } - - // return probabilities or log odds ratio - if( !logR ) - { -#ifdef _OPENMP -#pragma omp parallel for -#endif - for ( int j = 0; j < (int) res.size(); j++ ) - { - res[j] = sigmoid( res[j] ); - } - } - - return res; -} - -//implementations for weak classifier - -ClfOnlineStump::ClfOnlineStump() -{ - _trained = false; - _ind = -1; - init(); -} - -ClfOnlineStump::ClfOnlineStump( int ind ) -{ - _trained = false; - _ind = ind; - init(); -} -void ClfOnlineStump::init() -{ - _mu0 = 0; - _mu1 = 0; - _sig0 = 1; - _sig1 = 1; - _lRate = 0.85f; - _trained = false; -} - -void ClfOnlineStump::update( const Mat& posx, const Mat& negx, const Mat_& /*posw*/, const Mat_& /*negw*/) -{ - //std::cout << " ClfOnlineStump::update" << _ind << std::endl; - float posmu = 0.0, negmu = 0.0; - if( posx.cols > 0 ) - posmu = float( mean( posx.col( _ind ) )[0] ); - if( negx.cols > 0 ) - negmu = float( mean( negx.col( _ind ) )[0] ); - - if( _trained ) - { - if( posx.cols > 0 ) - { - _mu1 = ( _lRate * _mu1 + ( 1 - _lRate ) * posmu ); - cv::Mat diff = posx.col( _ind ) - _mu1; - _sig1 = _lRate * _sig1 + ( 1 - _lRate ) * float( mean( diff.mul( diff ) )[0] ); - } - if( negx.cols > 0 ) - { - _mu0 = ( _lRate * _mu0 + ( 1 - _lRate ) * negmu ); - cv::Mat diff = negx.col( _ind ) - _mu0; - _sig0 = _lRate * _sig0 + ( 1 - _lRate ) * float( mean( diff.mul( diff ) )[0] ); - } - - _q = ( _mu1 - _mu0 ) / 2; - _s = sign( _mu1 - _mu0 ); - _log_n0 = std::log( float( 1.0f / pow( _sig0, 0.5f ) ) ); - _log_n1 = std::log( float( 1.0f / pow( _sig1, 0.5f ) ) ); - //_e1 = -1.0f/(2.0f*_sig1+1e-99f); - //_e0 = -1.0f/(2.0f*_sig0+1e-99f); - _e1 = -1.0f / ( 2.0f * _sig1 + std::numeric_limits::min() ); - _e0 = -1.0f / ( 2.0f * _sig0 + std::numeric_limits::min() ); - - } - else - { - _trained = true; - if( posx.cols > 0 ) - { - _mu1 = posmu; - cv::Scalar scal_mean, scal_std_dev; - cv::meanStdDev( posx.col( _ind ), scal_mean, scal_std_dev ); - _sig1 = float( scal_std_dev[0] ) * float( scal_std_dev[0] ) + 1e-9f; - } - - if( negx.cols > 0 ) - { - _mu0 = negmu; - cv::Scalar scal_mean, scal_std_dev; - cv::meanStdDev( negx.col( _ind ), scal_mean, scal_std_dev ); - _sig0 = float( scal_std_dev[0] ) * float( scal_std_dev[0] ) + 1e-9f; - } - - _q = ( _mu1 - _mu0 ) / 2; - _s = sign( _mu1 - _mu0 ); - _log_n0 = std::log( float( 1.0f / pow( _sig0, 0.5f ) ) ); - _log_n1 = std::log( float( 1.0f / pow( _sig1, 0.5f ) ) ); - //_e1 = -1.0f/(2.0f*_sig1+1e-99f); - //_e0 = -1.0f/(2.0f*_sig0+1e-99f); - _e1 = -1.0f / ( 2.0f * _sig1 + std::numeric_limits::min() ); - _e0 = -1.0f / ( 2.0f * _sig0 + std::numeric_limits::min() ); - } -} - -bool ClfOnlineStump::classify( const Mat& x, int i ) -{ - float xx = x.at( i, _ind ); - double log_p0 = ( xx - _mu0 ) * ( xx - _mu0 ) * _e0 + _log_n0; - double log_p1 = ( xx - _mu1 ) * ( xx - _mu1 ) * _e1 + _log_n1; - return log_p1 > log_p0; -} - -float ClfOnlineStump::classifyF( const Mat& x, int i ) -{ - float xx = x.at( i, _ind ); - double log_p0 = ( xx - _mu0 ) * ( xx - _mu0 ) * _e0 + _log_n0; - double log_p1 = ( xx - _mu1 ) * ( xx - _mu1 ) * _e1 + _log_n1; - return float( log_p1 - log_p0 ); -} - -inline std::vector ClfOnlineStump::classifySetF( const Mat& x ) -{ - std::vector res( x.rows ); - -#ifdef _OPENMP -#pragma omp parallel for -#endif - for ( int k = 0; k < (int) res.size(); k++ ) - { - res[k] = classifyF( x, k ); - } - return res; -} - -} /* namespace cv */ diff --git a/modules/tracking/src/tracker.cpp b/modules/tracking/src/tracker.cpp index d6c0a8d0a61..42c9ccaa69a 100644 --- a/modules/tracking/src/tracker.cpp +++ b/modules/tracking/src/tracker.cpp @@ -4,20 +4,6 @@ #include "precomp.hpp" -namespace cv { -inline namespace tracking { - -Tracker::Tracker() -{ - // nothing -} - -Tracker::~Tracker() -{ - // nothing -} - -}} // namespace - +// see modules/video/src/tracking/tracker.cpp #include "legacy/tracker.legacy.hpp" diff --git a/modules/tracking/src/trackerBoosting.cpp b/modules/tracking/src/trackerBoosting.cpp index e3dcc1998f3..9536e8db235 100644 --- a/modules/tracking/src/trackerBoosting.cpp +++ b/modules/tracking/src/trackerBoosting.cpp @@ -136,7 +136,7 @@ bool TrackerBoostingImpl::initImpl( const Mat& image, const Rect2d& boundingBox CSparameters.overlap = params.samplerOverlap; CSparameters.searchFactor = params.samplerSearchFactor; - Ptr CSSampler = Ptr( new TrackerSamplerCS( CSparameters ) ); + Ptr CSSampler = Ptr( new TrackerSamplerCS( CSparameters ) ); if( !sampler->addTrackerSamplerAlgorithm( CSSampler ) ) return false; @@ -155,11 +155,11 @@ bool TrackerBoostingImpl::initImpl( const Mat& image, const Rect2d& boundingBox Rect ROI = CSSampler.staticCast()->getROI(); //compute HAAR features - TrackerFeatureHAAR::Params HAARparameters; + TrackerContribFeatureHAAR::Params HAARparameters; HAARparameters.numFeatures = params.featureSetNumFeatures; HAARparameters.isIntegral = true; HAARparameters.rectSize = Size( static_cast(boundingBox.width), static_cast(boundingBox.height) ); - Ptr trackerFeature = Ptr( new TrackerFeatureHAAR( HAARparameters ) ); + Ptr trackerFeature = Ptr( new TrackerContribFeatureHAAR( HAARparameters ) ); if( !featureSet->addTrackerFeature( trackerFeature ) ) return false; @@ -179,11 +179,11 @@ bool TrackerBoostingImpl::initImpl( const Mat& image, const Rect2d& boundingBox for ( int i = 0; i < params.iterationInit; i++ ) { //compute temp features - TrackerFeatureHAAR::Params HAARparameters2; + TrackerContribFeatureHAAR::Params HAARparameters2; HAARparameters2.numFeatures = static_cast( posSamples.size() + negSamples.size() ); HAARparameters2.isIntegral = true; HAARparameters2.rectSize = Size( static_cast(boundingBox.width), static_cast(boundingBox.height) ); - Ptr trackerFeature2 = Ptr( new TrackerFeatureHAAR( HAARparameters2 ) ); + Ptr trackerFeature2 = Ptr( new TrackerContribFeatureHAAR( HAARparameters2 ) ); model.staticCast()->setMode( TrackerBoostingModel::MODE_NEGATIVE, negSamples ); model->modelEstimation( negResponse ); @@ -198,8 +198,8 @@ bool TrackerBoostingImpl::initImpl( const Mat& image, const Rect2d& boundingBox { if( replacedClassifier[j] != -1 && swappedClassified[j] != -1 ) { - trackerFeature.staticCast()->swapFeature( replacedClassifier[j], swappedClassified[j] ); - trackerFeature.staticCast()->swapFeature( swappedClassified[j], trackerFeature2->getFeatureAt( (int)j ) ); + trackerFeature.staticCast()->swapFeature( replacedClassifier[j], swappedClassified[j] ); + trackerFeature.staticCast()->swapFeature( swappedClassified[j], trackerFeature2->getFeatureAt( (int)j ) ); } } } @@ -244,7 +244,7 @@ bool TrackerBoostingImpl::updateImpl( const Mat& image, Rect2d& boundingBox ) Mat response; std::vector classifiers = model->getTrackerStateEstimator().staticCast()->computeSelectedWeakClassifier(); - Ptr extractor = featureSet->getTrackerFeature()[0].second.staticCast(); + Ptr extractor = featureSet->getTrackerFeature()[0].second.staticCast(); extractor->extractSelected( classifiers, detectSamples, response ); responses.push_back( response ); @@ -292,11 +292,11 @@ bool TrackerBoostingImpl::updateImpl( const Mat& image, Rect2d& boundingBox ) const std::vector negResponse = featureSet->getResponses(); //compute temp features - TrackerFeatureHAAR::Params HAARparameters2; + TrackerContribFeatureHAAR::Params HAARparameters2; HAARparameters2.numFeatures = static_cast( posSamples.size() + negSamples.size() ); HAARparameters2.isIntegral = true; HAARparameters2.rectSize = Size( static_cast(boundingBox.width), static_cast(boundingBox.height) ); - Ptr trackerFeature2 = Ptr( new TrackerFeatureHAAR( HAARparameters2 ) ); + Ptr trackerFeature2 = Ptr( new TrackerContribFeatureHAAR( HAARparameters2 ) ); //model estimate model.staticCast()->setMode( TrackerBoostingModel::MODE_NEGATIVE, negSamples ); @@ -314,8 +314,8 @@ bool TrackerBoostingImpl::updateImpl( const Mat& image, Rect2d& boundingBox ) { if( replacedClassifier[j] != -1 && swappedClassified[j] != -1 ) { - featureSet->getTrackerFeature().at( 0 ).second.staticCast()->swapFeature( replacedClassifier[j], swappedClassified[j] ); - featureSet->getTrackerFeature().at( 0 ).second.staticCast()->swapFeature( swappedClassified[j], + featureSet->getTrackerFeature().at( 0 ).second.staticCast()->swapFeature( replacedClassifier[j], swappedClassified[j] ); + featureSet->getTrackerFeature().at( 0 ).second.staticCast()->swapFeature( swappedClassified[j], trackerFeature2->getFeatureAt( (int)j ) ); } } diff --git a/modules/tracking/src/trackerFeature.cpp b/modules/tracking/src/trackerFeature.cpp index 3f86fb5d0b8..e216d5873ba 100644 --- a/modules/tracking/src/trackerFeature.cpp +++ b/modules/tracking/src/trackerFeature.cpp @@ -46,23 +46,15 @@ namespace detail { inline namespace tracking { /* - * TrackerFeature + * TrackerContribFeature */ -TrackerFeature::~TrackerFeature() +TrackerContribFeature::~TrackerContribFeature() { } -void TrackerFeature::compute( const std::vector& images, Mat& response ) -{ - if( images.empty() ) - return; - - computeImpl( images, response ); -} - -Ptr TrackerFeature::create( const String& trackerFeatureType ) +Ptr TrackerContribFeature::create( const String& trackerFeatureType ) { if( trackerFeatureType.find( "FEATURE2D" ) == 0 ) { @@ -82,7 +74,7 @@ Ptr TrackerFeature::create( const String& trackerFeatureType ) if( trackerFeatureType.find( "HAAR" ) == 0 ) { - return Ptr( new TrackerFeatureHAAR() ); + return Ptr( new TrackerContribFeatureHAAR() ); } if( trackerFeatureType.find( "LBP" ) == 0 ) @@ -93,7 +85,7 @@ Ptr TrackerFeature::create( const String& trackerFeatureType ) CV_Error( -1, "Tracker feature type not supported" ); } -String TrackerFeature::getClassName() const +String TrackerContribFeature::getClassName() const { return className; } @@ -145,21 +137,21 @@ void TrackerFeatureHOG::selection( Mat& /*response*/, int /*npoints*/) } /** - * TrackerFeatureHAAR + * TrackerContribFeatureHAAR */ /** * Parameters */ -TrackerFeatureHAAR::Params::Params() +TrackerContribFeatureHAAR::Params::Params() { numFeatures = 250; rectSize = Size( 100, 100 ); isIntegral = false; } -TrackerFeatureHAAR::TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶meters ) : +TrackerContribFeatureHAAR::TrackerContribFeatureHAAR( const TrackerContribFeatureHAAR::Params ¶meters ) : params( parameters ) { className = "HAAR"; @@ -171,23 +163,23 @@ TrackerFeatureHAAR::TrackerFeatureHAAR( const TrackerFeatureHAAR::Params ¶me featureEvaluator->init( &haarParams, 1, params.rectSize ); } -TrackerFeatureHAAR::~TrackerFeatureHAAR() +TrackerContribFeatureHAAR::~TrackerContribFeatureHAAR() { } -CvHaarEvaluator::FeatureHaar& TrackerFeatureHAAR::getFeatureAt( int id ) +CvHaarEvaluator::FeatureHaar& TrackerContribFeatureHAAR::getFeatureAt( int id ) { return featureEvaluator->getFeatures( id ); } -bool TrackerFeatureHAAR::swapFeature( int id, CvHaarEvaluator::FeatureHaar& feature ) +bool TrackerContribFeatureHAAR::swapFeature( int id, CvHaarEvaluator::FeatureHaar& feature ) { featureEvaluator->getFeatures( id ) = feature; return true; } -bool TrackerFeatureHAAR::swapFeature( int source, int target ) +bool TrackerContribFeatureHAAR::swapFeature( int source, int target ) { CvHaarEvaluator::FeatureHaar feature = featureEvaluator->getFeatures( source ); featureEvaluator->getFeatures( source ) = featureEvaluator->getFeatures( target ); @@ -195,7 +187,7 @@ bool TrackerFeatureHAAR::swapFeature( int source, int target ) return true; } -bool TrackerFeatureHAAR::extractSelected( const std::vector selFeatures, const std::vector& images, Mat& response ) +bool TrackerContribFeatureHAAR::extractSelected( const std::vector selFeatures, const std::vector& images, Mat& response ) { if( images.empty() ) { @@ -264,7 +256,7 @@ class Parallel_compute : public cv::ParallelLoopBody } }; -bool TrackerFeatureHAAR::computeImpl( const std::vector& images, Mat& response ) +bool TrackerContribFeatureHAAR::computeImpl( const std::vector& images, Mat& response ) { if( images.empty() ) { @@ -294,7 +286,7 @@ bool TrackerFeatureHAAR::computeImpl( const std::vector& images, Mat& respo return true; } -void TrackerFeatureHAAR::selection( Mat& /*response*/, int /*npoints*/) +void TrackerContribFeatureHAAR::selection( Mat& /*response*/, int /*npoints*/) { } diff --git a/modules/tracking/src/trackerFeatureSet.cpp b/modules/tracking/src/trackerFeatureSet.cpp index dfa847a5123..e391ea9f9aa 100644 --- a/modules/tracking/src/trackerFeatureSet.cpp +++ b/modules/tracking/src/trackerFeatureSet.cpp @@ -46,13 +46,13 @@ namespace detail { inline namespace tracking { /* - * TrackerFeatureSet + * TrackerContribFeatureSet */ /* * Constructor */ -TrackerFeatureSet::TrackerFeatureSet() +TrackerContribFeatureSet::TrackerContribFeatureSet() { blockAddTrackerFeature = false; } @@ -60,12 +60,12 @@ TrackerFeatureSet::TrackerFeatureSet() /* * Destructor */ -TrackerFeatureSet::~TrackerFeatureSet() +TrackerContribFeatureSet::~TrackerContribFeatureSet() { } -void TrackerFeatureSet::extraction( const std::vector& images ) +void TrackerContribFeatureSet::extraction( const std::vector& images ) { clearResponses(); @@ -84,23 +84,23 @@ void TrackerFeatureSet::extraction( const std::vector& images ) } } -void TrackerFeatureSet::selection() +void TrackerContribFeatureSet::selection() { } -void TrackerFeatureSet::removeOutliers() +void TrackerContribFeatureSet::removeOutliers() { } -bool TrackerFeatureSet::addTrackerFeature( String trackerFeatureType ) +bool TrackerContribFeatureSet::addTrackerFeature( String trackerFeatureType ) { if( blockAddTrackerFeature ) { return false; } - Ptr feature = TrackerFeature::create( trackerFeatureType ); + Ptr feature = TrackerContribFeature::create( trackerFeatureType ); if (!feature) { @@ -112,7 +112,7 @@ bool TrackerFeatureSet::addTrackerFeature( String trackerFeatureType ) return true; } -bool TrackerFeatureSet::addTrackerFeature( Ptr& feature ) +bool TrackerContribFeatureSet::addTrackerFeature( Ptr& feature ) { if( blockAddTrackerFeature ) { @@ -125,17 +125,17 @@ bool TrackerFeatureSet::addTrackerFeature( Ptr& feature ) return true; } -const std::vector > >& TrackerFeatureSet::getTrackerFeature() const +const std::vector > >& TrackerContribFeatureSet::getTrackerFeature() const { return features; } -const std::vector& TrackerFeatureSet::getResponses() const +const std::vector& TrackerContribFeatureSet::getResponses() const { return responses; } -void TrackerFeatureSet::clearResponses() +void TrackerContribFeatureSet::clearResponses() { responses.clear(); } diff --git a/modules/tracking/src/trackerMIL.cpp b/modules/tracking/src/trackerMIL.cpp deleted file mode 100644 index 3d3c22c1f73..00000000000 --- a/modules/tracking/src/trackerMIL.cpp +++ /dev/null @@ -1,265 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#include "precomp.hpp" -#include "trackerMILModel.hpp" - -namespace cv { -inline namespace tracking { -namespace impl { - -class TrackerMILImpl CV_FINAL : public TrackerMIL -{ -public: - TrackerMILImpl(const TrackerMIL::Params ¶meters); - - virtual void init(InputArray image, const Rect& boundingBox) CV_OVERRIDE; - virtual bool update(InputArray image, Rect& boundingBox) CV_OVERRIDE; - - - void compute_integral( const Mat & img, Mat & ii_img ); - - TrackerMIL::Params params; - - Ptr model; - Ptr sampler; - Ptr featureSet; -}; - -} // namespace - -TrackerMIL::Params::Params() -{ - samplerInitInRadius = 3; - samplerSearchWinSize = 25; - samplerInitMaxNegNum = 65; - samplerTrackInRadius = 4; - samplerTrackMaxPosNum = 100000; - samplerTrackMaxNegNum = 65; - featureSetNumFeatures = 250; -} - -namespace impl { - -TrackerMILImpl::TrackerMILImpl(const TrackerMIL::Params ¶meters) - : params(parameters) -{ - // nothing -} - -void TrackerMILImpl::compute_integral( const Mat & img, Mat & ii_img ) -{ - Mat ii; - std::vector ii_imgs; - integral( img, ii, CV_32F ); // FIXIT split first - split( ii, ii_imgs ); - ii_img = ii_imgs[0]; -} - -void TrackerMILImpl::init(InputArray image, const Rect& boundingBox) -{ - sampler = makePtr(); - featureSet = makePtr(); - - Mat intImage; - compute_integral(image.getMat(), intImage); - TrackerSamplerCSC::Params CSCparameters; - CSCparameters.initInRad = params.samplerInitInRadius; - CSCparameters.searchWinSize = params.samplerSearchWinSize; - CSCparameters.initMaxNegNum = params.samplerInitMaxNegNum; - CSCparameters.trackInPosRad = params.samplerTrackInRadius; - CSCparameters.trackMaxPosNum = params.samplerTrackMaxPosNum; - CSCparameters.trackMaxNegNum = params.samplerTrackMaxNegNum; - - Ptr CSCSampler = makePtr(CSCparameters); - CV_Assert(sampler->addTrackerSamplerAlgorithm(CSCSampler)); - - //or add CSC sampler with default parameters - //sampler->addTrackerSamplerAlgorithm( "CSC" ); - - //Positive sampling - CSCSampler.staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_POS ); - sampler->sampling( intImage, boundingBox ); - std::vector posSamples = sampler->getSamples(); - - //Negative sampling - CSCSampler.staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_NEG ); - sampler->sampling( intImage, boundingBox ); - std::vector negSamples = sampler->getSamples(); - - CV_Assert(!posSamples.empty()); - CV_Assert(!negSamples.empty()); - - //compute HAAR features - TrackerFeatureHAAR::Params HAARparameters; - HAARparameters.numFeatures = params.featureSetNumFeatures; - HAARparameters.rectSize = Size( (int)boundingBox.width, (int)boundingBox.height ); - HAARparameters.isIntegral = true; - Ptr trackerFeature = Ptr( new TrackerFeatureHAAR( HAARparameters ) ); - featureSet->addTrackerFeature( trackerFeature ); - - featureSet->extraction( posSamples ); - const std::vector posResponse = featureSet->getResponses(); - - featureSet->extraction( negSamples ); - const std::vector negResponse = featureSet->getResponses(); - - model = makePtr(boundingBox); - Ptr stateEstimator = Ptr( - new TrackerStateEstimatorMILBoosting( params.featureSetNumFeatures ) ); - model->setTrackerStateEstimator( stateEstimator ); - - //Run model estimation and update - model.staticCast()->setMode( TrackerMILModel::MODE_POSITIVE, posSamples ); - model->modelEstimation( posResponse ); - model.staticCast()->setMode( TrackerMILModel::MODE_NEGATIVE, negSamples ); - model->modelEstimation( negResponse ); - model->modelUpdate(); -} - -bool TrackerMILImpl::update(InputArray image, Rect& boundingBox) -{ - Mat intImage; - compute_integral(image.getMat(), intImage); - - //get the last location [AAM] X(k-1) - Ptr lastLocation = model->getLastTargetState(); - Rect lastBoundingBox( (int)lastLocation->getTargetPosition().x, (int)lastLocation->getTargetPosition().y, lastLocation->getTargetWidth(), - lastLocation->getTargetHeight() ); - - //sampling new frame based on last location - ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_DETECT ); - sampler->sampling( intImage, lastBoundingBox ); - std::vector detectSamples = sampler->getSamples(); - if( detectSamples.empty() ) - return false; - - /*//TODO debug samples - Mat f; - image.copyTo(f); - - for( size_t i = 0; i < detectSamples.size(); i=i+10 ) - { - Size sz; - Point off; - detectSamples.at(i).locateROI(sz, off); - rectangle(f, Rect(off.x,off.y,detectSamples.at(i).cols,detectSamples.at(i).rows), Scalar(255,0,0), 1); - }*/ - - //extract features from new samples - featureSet->extraction( detectSamples ); - std::vector response = featureSet->getResponses(); - - //predict new location - ConfidenceMap cmap; - model.staticCast()->setMode( TrackerMILModel::MODE_ESTIMATON, detectSamples ); - model.staticCast()->responseToConfidenceMap( response, cmap ); - model->getTrackerStateEstimator().staticCast()->setCurrentConfidenceMap( cmap ); - - if( !model->runStateEstimator() ) - { - return false; - } - - Ptr currentState = model->getLastTargetState(); - boundingBox = Rect( (int)currentState->getTargetPosition().x, (int)currentState->getTargetPosition().y, currentState->getTargetWidth(), - currentState->getTargetHeight() ); - - /*//TODO debug - rectangle(f, lastBoundingBox, Scalar(0,255,0), 1); - rectangle(f, boundingBox, Scalar(0,0,255), 1); - imshow("f", f); - //waitKey( 0 );*/ - - //sampling new frame based on new location - //Positive sampling - ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_POS ); - sampler->sampling( intImage, boundingBox ); - std::vector posSamples = sampler->getSamples(); - - //Negative sampling - ( sampler->getSamplers().at( 0 ).second ).staticCast()->setMode( TrackerSamplerCSC::MODE_INIT_NEG ); - sampler->sampling( intImage, boundingBox ); - std::vector negSamples = sampler->getSamples(); - - if( posSamples.empty() || negSamples.empty() ) - return false; - - //extract features - featureSet->extraction( posSamples ); - std::vector posResponse = featureSet->getResponses(); - - featureSet->extraction( negSamples ); - std::vector negResponse = featureSet->getResponses(); - - //model estimate - model.staticCast()->setMode( TrackerMILModel::MODE_POSITIVE, posSamples ); - model->modelEstimation( posResponse ); - model.staticCast()->setMode( TrackerMILModel::MODE_NEGATIVE, negSamples ); - model->modelEstimation( negResponse ); - - //model update - model->modelUpdate(); - - return true; -} - -} // namespace - - -TrackerMIL::TrackerMIL() -{ - // nothing -} - -TrackerMIL::~TrackerMIL() -{ - // nothing -} - -Ptr TrackerMIL::create(const TrackerMIL::Params ¶meters) -{ - return makePtr(parameters); -} - -}} // namespace - -#include "legacy/trackerMIL.legacy.hpp" diff --git a/modules/tracking/src/trackerMILModel.cpp b/modules/tracking/src/trackerMILModel.cpp deleted file mode 100644 index 0d792890470..00000000000 --- a/modules/tracking/src/trackerMILModel.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#include "precomp.hpp" -#include "trackerMILModel.hpp" - -/** - * TrackerMILModel - */ - -namespace cv { -inline namespace tracking { -namespace impl { - -TrackerMILModel::TrackerMILModel( const Rect& boundingBox ) -{ - currentSample.clear(); - mode = MODE_POSITIVE; - width = boundingBox.width; - height = boundingBox.height; - - Ptr initState = Ptr( - new TrackerStateEstimatorMILBoosting::TrackerMILTargetState( Point2f( (float)boundingBox.x, (float)boundingBox.y ), boundingBox.width, boundingBox.height, - true, Mat() ) ); - trajectory.push_back( initState ); -} - -void TrackerMILModel::responseToConfidenceMap( const std::vector& responses, ConfidenceMap& confidenceMap ) -{ - if( currentSample.empty() ) - { - CV_Error( -1, "The samples in Model estimation are empty" ); - } - - for ( size_t i = 0; i < responses.size(); i++ ) - { - //for each column (one sample) there are #num_feature - //get informations from currentSample - for ( int j = 0; j < responses.at( i ).cols; j++ ) - { - - Size currentSize; - Point currentOfs; - currentSample.at( j ).locateROI( currentSize, currentOfs ); - bool foreground = false; - if( mode == MODE_POSITIVE || mode == MODE_ESTIMATON ) - { - foreground = true; - } - else if( mode == MODE_NEGATIVE ) - { - foreground = false; - } - - //get the column of the HAAR responses - Mat singleResponse = responses.at( i ).col( j ); - - //create the state - Ptr currentState = Ptr( - new TrackerStateEstimatorMILBoosting::TrackerMILTargetState( currentOfs, width, height, foreground, singleResponse ) ); - - confidenceMap.push_back( std::make_pair( currentState, 0.0f ) ); - - } - - } -} - -void TrackerMILModel::modelEstimationImpl( const std::vector& responses ) -{ - responseToConfidenceMap( responses, currentConfidenceMap ); - -} - -void TrackerMILModel::modelUpdateImpl() -{ - -} - -void TrackerMILModel::setMode( int trainingMode, const std::vector& samples ) -{ - currentSample.clear(); - currentSample = samples; - - mode = trainingMode; -} - -}}} // namespace diff --git a/modules/tracking/src/trackerMILModel.hpp b/modules/tracking/src/trackerMILModel.hpp deleted file mode 100644 index 41945be7aba..00000000000 --- a/modules/tracking/src/trackerMILModel.hpp +++ /dev/null @@ -1,102 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#ifndef __OPENCV_TRACKER_MIL_MODEL_HPP__ -#define __OPENCV_TRACKER_MIL_MODEL_HPP__ - -namespace cv { -inline namespace tracking { -namespace impl { - -/** - * \brief Implementation of TrackerModel for MIL algorithm - */ -class TrackerMILModel : public TrackerModel -{ - public: - enum - { - MODE_POSITIVE = 1, // mode for positive features - MODE_NEGATIVE = 2, // mode for negative features - MODE_ESTIMATON = 3 // mode for estimation step - }; - - /** - * \brief Constructor - * \param boundingBox The first boundingBox - */ - TrackerMILModel( const Rect& boundingBox ); - - /** - * \brief Destructor - */ - ~TrackerMILModel() - { - } - ; - - /** - * \brief Set the mode - */ - void setMode( int trainingMode, const std::vector& samples ); - - /** - * \brief Create the ConfidenceMap from a list of responses - * \param responses The list of the responses - * \param confidenceMap The output - */ - void responseToConfidenceMap( const std::vector& responses, ConfidenceMap& confidenceMap ); - - protected: - void modelEstimationImpl( const std::vector& responses ) CV_OVERRIDE; - void modelUpdateImpl() CV_OVERRIDE; - - private: - int mode; - std::vector currentSample; - - int width; //initial width of the boundingBox - int height; //initial height of the boundingBox -}; - -}}} // namespace - -#endif diff --git a/modules/tracking/src/legacy/trackerMIL.legacy.hpp b/modules/tracking/src/trackerMIL_legacy.cpp similarity index 83% rename from modules/tracking/src/legacy/trackerMIL.legacy.hpp rename to modules/tracking/src/trackerMIL_legacy.cpp index 8e3a1b4300f..d6dc3cd2cdc 100644 --- a/modules/tracking/src/legacy/trackerMIL.legacy.hpp +++ b/modules/tracking/src/trackerMIL_legacy.cpp @@ -39,6 +39,7 @@ // //M*/ +#include "precomp.hpp" #include "opencv2/tracking/tracking_legacy.hpp" namespace cv { @@ -49,36 +50,41 @@ namespace impl { class TrackerMILImpl CV_FINAL : public legacy::TrackerMIL { public: - cv::tracking::impl::TrackerMILImpl impl; + Ptr impl; + legacy::TrackerMIL::Params params; TrackerMILImpl(const legacy::TrackerMIL::Params ¶meters) - : impl(parameters) + : impl(cv::TrackerMIL::create(parameters)) + , params(parameters) { isInit = false; } void read(const FileNode& fn) CV_OVERRIDE { - static_cast(impl.params).read(fn); + params.read(fn); + CV_Error(Error::StsNotImplemented, "Can't update legacy tracker wrapper"); } void write(FileStorage& fs) const CV_OVERRIDE { - static_cast(impl.params).write(fs); + params.write(fs); } - bool initImpl(const Mat& image, const Rect2d& boundingBox) CV_OVERRIDE + bool initImpl(const Mat& image, const Rect2d& boundingBox2d) CV_OVERRIDE { - impl.init(image, boundingBox); - model = impl.model; - featureSet = impl.featureSet; - sampler = impl.sampler; + int x1 = cvRound(boundingBox2d.x); + int y1 = cvRound(boundingBox2d.y); + int x2 = cvRound(boundingBox2d.x + boundingBox2d.width); + int y2 = cvRound(boundingBox2d.y + boundingBox2d.height); + Rect boundingBox = Rect(x1, y1, x2 - x1, y2 - y1) & Rect(Point(0, 0), image.size()); + impl->init(image, boundingBox); isInit = true; return true; } bool updateImpl(const Mat& image, Rect2d& boundingBox) CV_OVERRIDE { Rect bb; - bool res = impl.update(image, bb); + bool res = impl->update(image, bb); boundingBox = bb; return res; } diff --git a/modules/tracking/src/trackerModel.cpp b/modules/tracking/src/trackerModel.cpp deleted file mode 100644 index 065721f3f03..00000000000 --- a/modules/tracking/src/trackerModel.cpp +++ /dev/null @@ -1,178 +0,0 @@ -/*M/////////////////////////////////////////////////////////////////////////////////////// - // - // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. - // - // By downloading, copying, installing or using the software you agree to this license. - // If you do not agree to this license, do not download, install, - // copy or use the software. - // - // - // License Agreement - // For Open Source Computer Vision Library - // - // Copyright (C) 2013, OpenCV Foundation, all rights reserved. - // Third party copyrights are property of their respective owners. - // - // Redistribution and use in source and binary forms, with or without modification, - // are permitted provided that the following conditions are met: - // - // * Redistribution's of source code must retain the above copyright notice, - // this list of conditions and the following disclaimer. - // - // * Redistribution's in binary form must reproduce the above copyright notice, - // this list of conditions and the following disclaimer in the documentation - // and/or other materials provided with the distribution. - // - // * The name of the copyright holders may not be used to endorse or promote products - // derived from this software without specific prior written permission. - // - // This software is provided by the copyright holders and contributors "as is" and - // any express or implied warranties, including, but not limited to, the implied - // warranties of merchantability and fitness for a particular purpose are disclaimed. - // In no event shall the Intel Corporation or contributors be liable for any direct, - // indirect, incidental, special, exemplary, or consequential damages - // (including, but not limited to, procurement of substitute goods or services; - // loss of use, data, or profits; or business interruption) however caused - // and on any theory of liability, whether in contract, strict liability, - // or tort (including negligence or otherwise) arising in any way out of - // the use of this software, even if advised of the possibility of such damage. - // - //M*/ - -#include "precomp.hpp" - -namespace cv { -namespace detail { -inline namespace tracking { - -/* - * TrackerModel - */ - -TrackerModel::TrackerModel() -{ - stateEstimator = Ptr(); - maxCMLength = 10; -} - -TrackerModel::~TrackerModel() -{ - -} - -bool TrackerModel::setTrackerStateEstimator( Ptr trackerStateEstimator ) -{ - if (stateEstimator.get()) - { - return false; - } - - stateEstimator = trackerStateEstimator; - return true; -} - -Ptr TrackerModel::getTrackerStateEstimator() const -{ - return stateEstimator; -} - -void TrackerModel::modelEstimation( const std::vector& responses ) -{ - modelEstimationImpl( responses ); - -} - -void TrackerModel::clearCurrentConfidenceMap() -{ - currentConfidenceMap.clear(); -} - -void TrackerModel::modelUpdate() -{ - modelUpdateImpl(); - - if( maxCMLength != -1 && (int) confidenceMaps.size() >= maxCMLength - 1 ) - { - int l = maxCMLength / 2; - confidenceMaps.erase( confidenceMaps.begin(), confidenceMaps.begin() + l ); - } - if( maxCMLength != -1 && (int) trajectory.size() >= maxCMLength - 1 ) - { - int l = maxCMLength / 2; - trajectory.erase( trajectory.begin(), trajectory.begin() + l ); - } - confidenceMaps.push_back( currentConfidenceMap ); - stateEstimator->update( confidenceMaps ); - - clearCurrentConfidenceMap(); - -} - -bool TrackerModel::runStateEstimator() -{ - if (!stateEstimator) - { - CV_Error( -1, "Tracker state estimator is not setted" ); - } - Ptr targetState = stateEstimator->estimate( confidenceMaps ); - if (!targetState) - return false; - - setLastTargetState( targetState ); - return true; -} - -void TrackerModel::setLastTargetState( const Ptr& lastTargetState ) -{ - trajectory.push_back( lastTargetState ); -} - -Ptr TrackerModel::getLastTargetState() const -{ - return trajectory.back(); -} - -const std::vector& TrackerModel::getConfidenceMaps() const -{ - return confidenceMaps; -} - -const ConfidenceMap& TrackerModel::getLastConfidenceMap() const -{ - return confidenceMaps.back(); -} - -/* - * TrackerTargetState - */ - -Point2f TrackerTargetState::getTargetPosition() const -{ - return targetPosition; -} - -void TrackerTargetState::setTargetPosition( const Point2f& position ) -{ - targetPosition = position; -} - -int TrackerTargetState::getTargetWidth() const -{ - return targetWidth; -} - -void TrackerTargetState::setTargetWidth( int width ) -{ - targetWidth = width; -} -int TrackerTargetState::getTargetHeight() const -{ - return targetHeight; -} - -void TrackerTargetState::setTargetHeight( int height ) -{ - targetHeight = height; -} - -}}} // namespace diff --git a/modules/tracking/src/trackerSampler.cpp b/modules/tracking/src/trackerSampler.cpp index 28f5d0f04fa..f4da8ba058c 100644 --- a/modules/tracking/src/trackerSampler.cpp +++ b/modules/tracking/src/trackerSampler.cpp @@ -46,13 +46,13 @@ namespace detail { inline namespace tracking { /* - * TrackerSampler + * TrackerContribSampler */ /* * Constructor */ -TrackerSampler::TrackerSampler() +TrackerContribSampler::TrackerContribSampler() { blockAddTrackerSampler = false; } @@ -60,12 +60,12 @@ TrackerSampler::TrackerSampler() /* * Destructor */ -TrackerSampler::~TrackerSampler() +TrackerContribSampler::~TrackerContribSampler() { } -void TrackerSampler::sampling( const Mat& image, Rect boundingBox ) +void TrackerContribSampler::sampling( const Mat& image, Rect boundingBox ) { clearSamples(); @@ -89,13 +89,13 @@ void TrackerSampler::sampling( const Mat& image, Rect boundingBox ) } } -bool TrackerSampler::addTrackerSamplerAlgorithm( String trackerSamplerAlgorithmType ) +bool TrackerContribSampler::addTrackerSamplerAlgorithm( String trackerSamplerAlgorithmType ) { if( blockAddTrackerSampler ) { return false; } - Ptr sampler = TrackerSamplerAlgorithm::create( trackerSamplerAlgorithmType ); + Ptr sampler = TrackerContribSamplerAlgorithm::create( trackerSamplerAlgorithmType ); if (!sampler) { @@ -107,7 +107,7 @@ bool TrackerSampler::addTrackerSamplerAlgorithm( String trackerSamplerAlgorithmT return true; } -bool TrackerSampler::addTrackerSamplerAlgorithm( Ptr& sampler ) +bool TrackerContribSampler::addTrackerSamplerAlgorithm( Ptr& sampler ) { if( blockAddTrackerSampler ) { @@ -125,17 +125,17 @@ bool TrackerSampler::addTrackerSamplerAlgorithm( Ptr& s return true; } -const std::vector > >& TrackerSampler::getSamplers() const +const std::vector > >& TrackerContribSampler::getSamplers() const { return samplers; } -const std::vector& TrackerSampler::getSamples() const +const std::vector& TrackerContribSampler::getSamples() const { return samples; } -void TrackerSampler::clearSamples() +void TrackerContribSampler::clearSamples() { samples.clear(); } diff --git a/modules/tracking/src/trackerSamplerAlgorithm.cpp b/modules/tracking/src/trackerSamplerAlgorithm.cpp index 39d1271ba79..75dd9a296a2 100644 --- a/modules/tracking/src/trackerSamplerAlgorithm.cpp +++ b/modules/tracking/src/trackerSamplerAlgorithm.cpp @@ -48,15 +48,15 @@ namespace detail { inline namespace tracking { /* - * TrackerSamplerAlgorithm + * TrackerContribSamplerAlgorithm */ -TrackerSamplerAlgorithm::~TrackerSamplerAlgorithm() +TrackerContribSamplerAlgorithm::~TrackerContribSamplerAlgorithm() { } -bool TrackerSamplerAlgorithm::sampling( const Mat& image, Rect boundingBox, std::vector& sample ) +bool TrackerContribSamplerAlgorithm::sampling(const Mat& image, const Rect& boundingBox, std::vector& sample) { if( image.empty() ) return false; @@ -64,11 +64,11 @@ bool TrackerSamplerAlgorithm::sampling( const Mat& image, Rect boundingBox, std: return samplingImpl( image, boundingBox, sample ); } -Ptr TrackerSamplerAlgorithm::create( const String& trackerSamplerType ) +Ptr TrackerContribSamplerAlgorithm::create( const String& trackerSamplerType ) { if( trackerSamplerType.find( "CSC" ) == 0 ) { - return Ptr( new TrackerSamplerCSC() ); + return Ptr( new TrackerContribSamplerCSC() ); } if( trackerSamplerType.find( "CS" ) == 0 ) @@ -76,23 +76,23 @@ Ptr TrackerSamplerAlgorithm::create( const String& trac return Ptr( new TrackerSamplerCS() ); } - CV_Error( -1, "Tracker sampler algorithm type not supported" ); + CV_Error(Error::StsNotImplemented, "Tracker sampler algorithm type not supported" ); } -String TrackerSamplerAlgorithm::getClassName() const +String TrackerContribSamplerAlgorithm::getClassName() const { return className; } /** - * TrackerSamplerCSC + * TrackerContribSamplerCSC */ /** * Parameters */ -TrackerSamplerCSC::Params::Params() +TrackerContribSamplerCSC::Params::Params() { initInRad = 3; initMaxNegNum = 65; @@ -103,7 +103,7 @@ TrackerSamplerCSC::Params::Params() } -TrackerSamplerCSC::TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meters ) : +TrackerContribSamplerCSC::TrackerContribSamplerCSC( const TrackerContribSamplerCSC::Params ¶meters ) : params( parameters ) { className = "CSC"; @@ -112,12 +112,12 @@ TrackerSamplerCSC::TrackerSamplerCSC( const TrackerSamplerCSC::Params ¶meter } -TrackerSamplerCSC::~TrackerSamplerCSC() +TrackerContribSamplerCSC::~TrackerContribSamplerCSC() { } -bool TrackerSamplerCSC::samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) +bool TrackerContribSamplerCSC::samplingImpl( const Mat& image, Rect boundingBox, std::vector& sample ) { float inrad = 0; float outrad = 0; @@ -159,12 +159,12 @@ bool TrackerSamplerCSC::samplingImpl( const Mat& image, Rect boundingBox, std::v return false; } -void TrackerSamplerCSC::setMode( int samplingMode ) +void TrackerContribSamplerCSC::setMode( int samplingMode ) { mode = samplingMode; } -std::vector TrackerSamplerCSC::sampleImage( const Mat& img, int x, int y, int w, int h, float inrad, float outrad, int maxnum ) +std::vector TrackerContribSamplerCSC::sampleImage( const Mat& img, int x, int y, int w, int h, float inrad, float outrad, int maxnum ) { int rowsz = img.rows - h - 1; int colsz = img.cols - w - 1; diff --git a/modules/tracking/src/trackerStateEstimator.cpp b/modules/tracking/src/trackerStateEstimator.cpp index d0f9dd36d9e..46b8a815b3b 100644 --- a/modules/tracking/src/trackerStateEstimator.cpp +++ b/modules/tracking/src/trackerStateEstimator.cpp @@ -45,33 +45,6 @@ namespace cv { namespace detail { inline namespace tracking { -/* - * TrackerStateEstimator - */ - -TrackerStateEstimator::~TrackerStateEstimator() -{ - -} - -Ptr TrackerStateEstimator::estimate( const std::vector& confidenceMaps ) -{ - if( confidenceMaps.empty() ) - return Ptr(); - - return estimateImpl( confidenceMaps ); - -} - -void TrackerStateEstimator::update( std::vector& confidenceMaps ) -{ - if( confidenceMaps.empty() ) - return; - - return updateImpl( confidenceMaps ); - -} - Ptr TrackerStateEstimator::create( const String& trackeStateEstimatorType ) { @@ -82,164 +55,14 @@ Ptr TrackerStateEstimator::create( const String& trackeSt if( trackeStateEstimatorType.find( "BOOSTING" ) == 0 ) { - return Ptr( new TrackerStateEstimatorMILBoosting() ); + CV_Error(Error::StsNotImplemented, "TrackerStateEstimatorMILBoosting API is not available"); + //return Ptr( new TrackerStateEstimatorMILBoosting() ); } CV_Error( -1, "Tracker state estimator type not supported" ); } -String TrackerStateEstimator::getClassName() const -{ - return className; -} - -/** - * TrackerStateEstimatorMILBoosting::TrackerMILTargetState - */ -TrackerStateEstimatorMILBoosting::TrackerMILTargetState::TrackerMILTargetState( const Point2f& position, int width, int height, bool foreground, - const Mat& features ) -{ - setTargetPosition( position ); - setTargetWidth( width ); - setTargetHeight( height ); - setTargetFg( foreground ); - setFeatures( features ); -} - -void TrackerStateEstimatorMILBoosting::TrackerMILTargetState::setTargetFg( bool foreground ) -{ - isTarget = foreground; -} - -void TrackerStateEstimatorMILBoosting::TrackerMILTargetState::setFeatures( const Mat& features ) -{ - targetFeatures = features; -} - -bool TrackerStateEstimatorMILBoosting::TrackerMILTargetState::isTargetFg() const -{ - return isTarget; -} - -Mat TrackerStateEstimatorMILBoosting::TrackerMILTargetState::getFeatures() const -{ - return targetFeatures; -} - -TrackerStateEstimatorMILBoosting::TrackerStateEstimatorMILBoosting( int nFeatures ) -{ - className = "BOOSTING"; - trained = false; - numFeatures = nFeatures; -} - -TrackerStateEstimatorMILBoosting::~TrackerStateEstimatorMILBoosting() -{ - -} -void TrackerStateEstimatorMILBoosting::setCurrentConfidenceMap( ConfidenceMap& confidenceMap ) -{ - currentConfidenceMap.clear(); - currentConfidenceMap = confidenceMap; -} - -uint TrackerStateEstimatorMILBoosting::max_idx( const std::vector &v ) -{ - const float* findPtr = & ( *std::max_element( v.begin(), v.end() ) ); - const float* beginPtr = & ( *v.begin() ); - return (uint) ( findPtr - beginPtr ); -} - -Ptr TrackerStateEstimatorMILBoosting::estimateImpl( const std::vector& /*confidenceMaps*/) -{ - //run ClfMilBoost classify in order to compute next location - if( currentConfidenceMap.empty() ) - return Ptr(); - - Mat positiveStates; - Mat negativeStates; - - prepareData( currentConfidenceMap, positiveStates, negativeStates ); - - std::vector prob = boostMILModel.classify( positiveStates ); - - int bestind = max_idx( prob ); - //float resp = prob[bestind]; - - return currentConfidenceMap.at( bestind ).first; -} - -void TrackerStateEstimatorMILBoosting::prepareData( const ConfidenceMap& confidenceMap, Mat& positive, Mat& negative ) -{ - - int posCounter = 0; - int negCounter = 0; - - for ( size_t i = 0; i < confidenceMap.size(); i++ ) - { - Ptr currentTargetState = confidenceMap.at( i ).first.staticCast(); - if( currentTargetState->isTargetFg() ) - posCounter++; - else - negCounter++; - } - - positive.create( posCounter, numFeatures, CV_32FC1 ); - negative.create( negCounter, numFeatures, CV_32FC1 ); - - //TODO change with mat fast access - //initialize trainData (positive and negative) - - int pc = 0; - int nc = 0; - for ( size_t i = 0; i < confidenceMap.size(); i++ ) - { - Ptr currentTargetState = confidenceMap.at( i ).first.staticCast(); - Mat stateFeatures = currentTargetState->getFeatures(); - - if( currentTargetState->isTargetFg() ) - { - for ( int j = 0; j < stateFeatures.rows; j++ ) - { - //fill the positive trainData with the value of the feature j for sample i - positive.at( pc, j ) = stateFeatures.at( j, 0 ); - } - pc++; - } - else - { - for ( int j = 0; j < stateFeatures.rows; j++ ) - { - //fill the negative trainData with the value of the feature j for sample i - negative.at( nc, j ) = stateFeatures.at( j, 0 ); - } - nc++; - } - - } -} - -void TrackerStateEstimatorMILBoosting::updateImpl( std::vector& confidenceMaps ) -{ - - if( !trained ) - { - //this is the first time that the classifier is built - //init MIL - boostMILModel.init(); - trained = true; - } - - ConfidenceMap lastConfidenceMap = confidenceMaps.back(); - Mat positiveStates; - Mat negativeStates; - - prepareData( lastConfidenceMap, positiveStates, negativeStates ); - //update MIL - boostMILModel.update( positiveStates, negativeStates ); - -} /** * TrackerStateEstimatorAdaBoosting diff --git a/modules/tracking/test/test_trackers.cpp b/modules/tracking/test/test_trackers.cpp index 222d272ca2b..1241bbba79b 100644 --- a/modules/tracking/test/test_trackers.cpp +++ b/modules/tracking/test/test_trackers.cpp @@ -58,379 +58,10 @@ const string TRACKING_DIR = "tracking"; const string FOLDER_IMG = "data"; const string FOLDER_OMIT_INIT = "initOmit"; -/* - * The Evaluation Methodologies are partially based on: - * ==================================================================================================================== - * [OTB] Y. Wu, J. Lim, and M.-H. Yang, "Online object tracking: A benchmark," in Computer Vision and Pattern Recognition (CVPR), 2013 - * - */ +// Check used "cmake" version in case of errors +// Check compiler command line options for /modules include +#include "video/test/test_trackers.impl.hpp" -enum BBTransformations -{ - NoTransform = 0, - CenterShiftLeft = 1, - CenterShiftRight = 2, - CenterShiftUp = 3, - CenterShiftDown = 4, - CornerShiftTopLeft = 5, - CornerShiftTopRight = 6, - CornerShiftBottomLeft = 7, - CornerShiftBottomRight = 8, - Scale_0_8 = 9, - Scale_0_9 = 10, - Scale_1_1 = 11, - Scale_1_2 = 12 -}; - -namespace { - -std::vector splitString(const std::string& s_, const std::string& delimiter) -{ - std::string s = s_; - std::vector token; - size_t pos = 0; - while ( ( pos = s.find( delimiter ) ) != std::string::npos ) - { - token.push_back( s.substr( 0, pos ) ); - s.erase( 0, pos + delimiter.length() ); - } - token.push_back( s ); - return token; -} - -float calcDistance(const Rect& a, const Rect& b) -{ - Point2f p_a( (float)(a.x + a.width / 2), (float)(a.y + a.height / 2) ); - Point2f p_b( (float)(b.x + b.width / 2), (float)(b.y + b.height / 2) ); - return sqrt( pow( p_a.x - p_b.x, 2 ) + pow( p_a.y - p_b.y, 2 ) ); -} - -float calcOverlap(const Rect& a, const Rect& b) -{ - float rectIntersectionArea = (float)(a & b).area(); - return rectIntersectionArea / (a.area() + b.area() - rectIntersectionArea); -} - -} // namespace - - -template -class TrackerTest -{ -public: - - TrackerTest(const Ptr& tracker, const string& video, float distanceThreshold, - float overlapThreshold, int shift = NoTransform, int segmentIdx = 1, int numSegments = 10); - ~TrackerTest() {} - void run(); - -protected: - void checkDataTest(); - - void distanceAndOverlapTest(); - - Ptr tracker; - string video; - std::vector bbs; - int startFrame; - string suffix; - string prefix; - float overlapThreshold; - float distanceThreshold; - int segmentIdx; - int shift; - int numSegments; - - int gtStartFrame; - int endFrame; - vector validSequence; - -private: - Rect applyShift(const Rect& bb); -}; - -template -TrackerTest::TrackerTest(const Ptr& _tracker, const string& _video, float _distanceThreshold, - float _overlapThreshold, int _shift, int _segmentIdx, int _numSegments ) : - tracker( _tracker ), - video( _video ), - overlapThreshold( _overlapThreshold ), - distanceThreshold( _distanceThreshold ), - segmentIdx(_segmentIdx), - shift(_shift), - numSegments(_numSegments) -{ - // nothing -} - -template -Rect TrackerTest::applyShift(const Rect& bb_) -{ - Rect bb = bb_; - Point center( bb.x + ( bb.width / 2 ), bb.y + ( bb.height / 2 ) ); - - int xLimit = bb.x + bb.width - 1; - int yLimit = bb.y + bb.height - 1; - - int h = 0; - int w = 0; - float ratio = 1.0; - - switch ( shift ) - { - case CenterShiftLeft: - bb.x = bb.x - (int)ceil( 0.1 * bb.width ); - break; - case CenterShiftRight: - bb.x = bb.x + (int)ceil( 0.1 * bb.width ); - break; - case CenterShiftUp: - bb.y = bb.y - (int)ceil( 0.1 * bb.height ); - break; - case CenterShiftDown: - bb.y = bb.y + (int)ceil( 0.1 * bb.height ); - break; - case CornerShiftTopLeft: - bb.x = (int)cvRound( bb.x - 0.1 * bb.width ); - bb.y = (int)cvRound( bb.y - 0.1 * bb.height ); - - bb.width = xLimit - bb.x + 1; - bb.height = yLimit - bb.y + 1; - break; - case CornerShiftTopRight: - xLimit = (int)cvRound( xLimit + 0.1 * bb.width ); - - bb.y = (int)cvRound( bb.y - 0.1 * bb.height ); - bb.width = xLimit - bb.x + 1; - bb.height = yLimit - bb.y + 1; - break; - case CornerShiftBottomLeft: - bb.x = (int)cvRound( bb.x - 0.1 * bb.width ); - yLimit = (int)cvRound( yLimit + 0.1 * bb.height ); - - bb.width = xLimit - bb.x + 1; - bb.height = yLimit - bb.y + 1; - break; - case CornerShiftBottomRight: - xLimit = (int)cvRound( xLimit + 0.1 * bb.width ); - yLimit = (int)cvRound( yLimit + 0.1 * bb.height ); - - bb.width = xLimit - bb.x + 1; - bb.height = yLimit - bb.y + 1; - break; - case Scale_0_8: - ratio = 0.8f; - w = (int)(ratio * bb.width); - h = (int)(ratio * bb.height); - - bb = Rect( center.x - ( w / 2 ), center.y - ( h / 2 ), w, h ); - break; - case Scale_0_9: - ratio = 0.9f; - w = (int)(ratio * bb.width); - h = (int)(ratio * bb.height); - - bb = Rect( center.x - ( w / 2 ), center.y - ( h / 2 ), w, h ); - break; - case 11: - //scale 1.1 - ratio = 1.1f; - w = (int)(ratio * bb.width); - h = (int)(ratio * bb.height); - - bb = Rect( center.x - ( w / 2 ), center.y - ( h / 2 ), w, h ); - break; - case 12: - //scale 1.2 - ratio = 1.2f; - w = (int)(ratio * bb.width); - h = (int)(ratio * bb.height); - - bb = Rect( center.x - ( w / 2 ), center.y - ( h / 2 ), w, h ); - break; - default: - break; - } - - return bb; -} - -template -void TrackerTest::distanceAndOverlapTest() -{ - bool initialized = false; - - int fc = ( startFrame - gtStartFrame ); - - bbs.at( fc ) = applyShift(bbs.at( fc )); - Rect currentBBi = bbs.at( fc ); - ROI_t currentBB(currentBBi); - float sumDistance = 0; - float sumOverlap = 0; - - string folder = cvtest::TS::ptr()->get_data_path() + "/" + TRACKING_DIR + "/" + video + "/" + FOLDER_IMG; - string videoPath = folder + "/" + video + ".webm"; - - VideoCapture c; - c.open(videoPath); - ASSERT_TRUE(c.isOpened()) << videoPath; -#if 0 - c.set(CAP_PROP_POS_FRAMES, startFrame); -#else - if (startFrame) - std::cout << "startFrame = " << startFrame << std::endl; - for (int i = 0; i < startFrame; i++) - { - Mat dummy_frame; - c >> dummy_frame; - ASSERT_FALSE(dummy_frame.empty()) << i << ": " << videoPath; - } -#endif - - for ( int frameCounter = startFrame; frameCounter < endFrame; frameCounter++ ) - { - Mat frame; - c >> frame; - - ASSERT_FALSE(frame.empty()) << "frameCounter=" << frameCounter << " video=" << videoPath; - if( !initialized ) - { -#if 0 - if( !tracker->init( frame, currentBB ) ) - { - FAIL()<< "Could not initialize tracker" << endl; - return; - } -#else - tracker->init(frame, currentBB); -#endif - std::cout << "frame size = " << frame.size() << std::endl; - initialized = true; - } - else if( initialized ) - { - if( frameCounter >= (int) bbs.size() ) - break; - tracker->update( frame, currentBB ); - } - float curDistance = calcDistance( currentBB, bbs.at( fc ) ); - float curOverlap = calcOverlap( currentBB, bbs.at( fc ) ); - -#ifdef DEBUG_TEST - Mat result; - repeat(frame, 1, 2, result); - rectangle(result, currentBB, Scalar(0, 255, 0), 1); - Rect roi2(frame.cols, 0, frame.cols, frame.rows); - rectangle(result(roi2), bbs.at(fc), Scalar(0, 0, 255), 1); - imshow("result", result); - waitKey(1); -#endif - - sumDistance += curDistance; - sumOverlap += curOverlap; - fc++; - } - - float meanDistance = sumDistance / (endFrame - startFrame); - float meanOverlap = sumOverlap / (endFrame - startFrame); - - EXPECT_LE(meanDistance, distanceThreshold); - EXPECT_GE(meanOverlap, overlapThreshold); -} - -template -void TrackerTest::checkDataTest() -{ - - FileStorage fs; - fs.open( cvtest::TS::ptr()->get_data_path() + TRACKING_DIR + "/" + video + "/" + video + ".yml", FileStorage::READ ); - fs["start"] >> startFrame; - fs["prefix"] >> prefix; - fs["suffix"] >> suffix; - fs.release(); - - string gtFile = cvtest::TS::ptr()->get_data_path() + TRACKING_DIR + "/" + video + "/gt.txt"; - std::ifstream gt; - //open the ground truth - gt.open( gtFile.c_str() ); - ASSERT_TRUE(gt.is_open()) << gtFile; - string line; - int bbCounter = 0; - while ( getline( gt, line ) ) - { - bbCounter++; - } - gt.close(); - - int seqLength = bbCounter; - for ( int i = startFrame; i < seqLength; i++ ) - { - validSequence.push_back( i ); - } - - //exclude from the images sequence, the frames where the target is occluded or out of view - string omitFile = cvtest::TS::ptr()->get_data_path() + TRACKING_DIR + "/" + video + "/" + FOLDER_OMIT_INIT + "/" + video + ".txt"; - std::ifstream omit; - omit.open( omitFile.c_str() ); - if( omit.is_open() ) - { - string omitLine; - while ( getline( omit, omitLine ) ) - { - vector tokens = splitString( omitLine, " " ); - int s_start = atoi( tokens.at( 0 ).c_str() ); - int s_end = atoi( tokens.at( 1 ).c_str() ); - for ( int k = s_start; k <= s_end; k++ ) - { - std::vector::iterator position = std::find( validSequence.begin(), validSequence.end(), k ); - if( position != validSequence.end() ) - validSequence.erase( position ); - } - } - } - omit.close(); - gtStartFrame = startFrame; - //compute the start and the and for each segment - int numFrame = (int)(validSequence.size() / numSegments); - startFrame += ( segmentIdx - 1 ) * numFrame; - endFrame = startFrame + numFrame; - - std::ifstream gt2; - //open the ground truth - gt2.open( gtFile.c_str() ); - ASSERT_TRUE(gt2.is_open()) << gtFile; - string line2; - int bbCounter2 = 0; - while ( getline( gt2, line2 ) ) - { - vector tokens = splitString( line2, "," ); - Rect bb( atoi( tokens.at( 0 ).c_str() ), atoi( tokens.at( 1 ).c_str() ), atoi( tokens.at( 2 ).c_str() ), atoi( tokens.at( 3 ).c_str() ) ); - ASSERT_EQ((size_t)4, tokens.size()) << "Incorrect ground truth file " << gtFile; - - bbs.push_back( bb ); - bbCounter2++; - } - gt2.close(); - - if( segmentIdx == numSegments ) - endFrame = (int)bbs.size(); -} - -template -void TrackerTest::run() -{ - srand( 1 ); // FIXIT remove that, ensure that there is no "rand()" in implementation - - ASSERT_TRUE(tracker); - - checkDataTest(); - - //check for failure - if( ::testing::Test::HasFatalFailure() ) - return; - - distanceAndOverlapTest(); -} /****************************************************************************************\ * Tests registrations * @@ -452,19 +83,6 @@ TEST_P(DistanceAndOverlap, MedianFlow) test.run(); } -TEST_P(DistanceAndOverlap, MIL) -{ - TrackerTest test(TrackerMIL::create(), dataset, 30, .65f, NoTransform); - test.run(); -} -#ifdef TEST_LEGACY -TEST_P(DistanceAndOverlap, MIL_legacy) -{ - TrackerTest test(legacy::TrackerMIL::create(), dataset, 30, .65f, NoTransform); - test.run(); -} -#endif - TEST_P(DistanceAndOverlap, Boosting) { TrackerTest test(legacy::TrackerBoosting::create(), dataset, 70, .7f, NoTransform); @@ -517,19 +135,6 @@ TEST_P(DistanceAndOverlap, Shifted_Data_MedianFlow) test.run(); } -TEST_P(DistanceAndOverlap, Shifted_Data_MIL) -{ - TrackerTest test(TrackerMIL::create(), dataset, 30, .6f, CenterShiftLeft); - test.run(); -} -#ifdef TEST_LEGACY -TEST_P(DistanceAndOverlap, Shifted_Data_MIL_legacy) -{ - TrackerTest test(legacy::TrackerMIL::create(), dataset, 30, .6f, CenterShiftLeft); - test.run(); -} -#endif - TEST_P(DistanceAndOverlap, Shifted_Data_Boosting) { TrackerTest test(legacy::TrackerBoosting::create(), dataset, 80, .65f, CenterShiftLeft); @@ -582,19 +187,6 @@ TEST_P(DistanceAndOverlap, Scaled_Data_MedianFlow) test.run(); } -TEST_P(DistanceAndOverlap, Scaled_Data_MIL) -{ - TrackerTest test(TrackerMIL::create(), dataset, 30, .7f, Scale_1_1); - test.run(); -} -#ifdef TEST_LEGACY -TEST_P(DistanceAndOverlap, Scaled_Data_MIL_legacy) -{ - TrackerTest test(legacy::TrackerMIL::create(), dataset, 30, .7f, Scale_1_1); - test.run(); -} -#endif - TEST_P(DistanceAndOverlap, Scaled_Data_Boosting) { TrackerTest test(legacy::TrackerBoosting::create(), dataset, 80, .7f, Scale_1_1); @@ -639,49 +231,6 @@ TEST_P(DistanceAndOverlap, Scaled_Data_CSRT_legacy) } #endif -TEST_P(DistanceAndOverlap, GOTURN) -{ - std::string model = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.prototxt"); - std::string weights = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.caffemodel", false); - cv::TrackerGOTURN::Params params; - params.modelTxt = model; - params.modelBin = weights; - TrackerTest test(TrackerGOTURN::create(params), dataset, 35, .35f, NoTransform); - test.run(); -} - INSTANTIATE_TEST_CASE_P(Tracking, DistanceAndOverlap, TESTSET_NAMES); - - -TEST(GOTURN, memory_usage) -{ - cv::Rect roi(145, 70, 85, 85); - - std::string model = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.prototxt"); - std::string weights = cvtest::findDataFile("dnn/gsoc2016-goturn/goturn.caffemodel", false); - cv::TrackerGOTURN::Params params; - params.modelTxt = model; - params.modelBin = weights; - cv::Ptr tracker = TrackerGOTURN::create(params); - - string inputVideo = cvtest::findDataFile("tracking/david/data/david.webm"); - cv::VideoCapture video(inputVideo); - ASSERT_TRUE(video.isOpened()) << inputVideo; - - cv::Mat frame; - video >> frame; - ASSERT_FALSE(frame.empty()) << inputVideo; - tracker->init(frame, roi); - string ground_truth_bb; - for (int nframes = 0; nframes < 15; ++nframes) - { - std::cout << "Frame: " << nframes << std::endl; - video >> frame; - bool res = tracker->update(frame, roi); - ASSERT_TRUE(res); - std::cout << "Predicted ROI: " << roi << std::endl; - } -} - }} // namespace From 8df9e849c4e091effe7f0a96b0718543cdcb866f Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Wed, 18 Nov 2020 12:01:40 +0000 Subject: [PATCH 23/29] sfm: fix build in non-C++11 mode --- modules/sfm/src/libmv_light/libmv/base/vector.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/sfm/src/libmv_light/libmv/base/vector.h b/modules/sfm/src/libmv_light/libmv/base/vector.h index 9740cfaf6fc..b3cae3d37ae 100644 --- a/modules/sfm/src/libmv_light/libmv/base/vector.h +++ b/modules/sfm/src/libmv_light/libmv/base/vector.h @@ -125,7 +125,11 @@ class vector { memcpy(data, data_, sizeof(*data)*size_); #else for (int i = 0; i < size_; ++i) +#ifdef CV_CXX11 new (&data[i]) T(std::move(data_[i])); +#else + new (&data[i]) T(data_[i]); +#endif for (int i = 0; i < size_; ++i) data_[i].~T(); #endif From 672ba702cd1b48604ab59a0a9784eee0b02ef7a8 Mon Sep 17 00:00:00 2001 From: crownedone <54454278+crownedone@users.noreply.github.com> Date: Wed, 18 Nov 2020 17:27:07 +0100 Subject: [PATCH 24/29] Merge pull request #2713 from crownedone:tbmr_features Tbmr features (purely topological adaptation on MSER) * initial commit * fix test data reusing stereomatching testdata. * fix incorrect function, ellipse notation, types and comments. * add required precomp.hpp, fix warnings. * fix naming * remove ocl for now. (we want to add opencl functionality later) * update readme * fix invalid module dependency. * add angle, minAxis and majAxis calculation. * formatting fixes. restructure component tree calculation. remove sort_indexes by using cv::sortIdx. * fix test using virtual data. * move tbmr to xfeatures2d. Add standard tests in xFeatures2d. * octave/scale and descriptor extraction using sift added * try fix the errors * fix parameter error * add scale for pyramid extraction and filter scaled points for duplicates. * fix exports * remove unrelated changes due to indentation * remove unrelated indentation changes in tests * externalize msd_pyramid add export wrappers * exchange malloc/calloc with AutoBuffer * fix warning fix correct license text --- modules/xfeatures2d/doc/xfeatures2d.bib | 16 +- .../include/opencv2/xfeatures2d.hpp | 36 +- modules/xfeatures2d/src/msd.cpp | 66 +- modules/xfeatures2d/src/msd_pyramid.hpp | 77 +++ modules/xfeatures2d/src/tbmr.cpp | 584 ++++++++++++++++++ modules/xfeatures2d/test/test_features2d.cpp | 6 + modules/xfeatures2d/test/test_keypoints.cpp | 6 + 7 files changed, 724 insertions(+), 67 deletions(-) create mode 100644 modules/xfeatures2d/src/msd_pyramid.hpp create mode 100644 modules/xfeatures2d/src/tbmr.cpp diff --git a/modules/xfeatures2d/doc/xfeatures2d.bib b/modules/xfeatures2d/doc/xfeatures2d.bib index b88be8bfe1e..6337a606d17 100644 --- a/modules/xfeatures2d/doc/xfeatures2d.bib +++ b/modules/xfeatures2d/doc/xfeatures2d.bib @@ -80,6 +80,20 @@ @article{Mikolajczyk2004 publisher = {Springer} } +@ARTICLE{Najman2014, + author={Y. {Xu} and P. {Monasse} and T. {Géraud} and L. {Najman}}, + journal={IEEE Transactions on Image Processing}, + title={Tree-Based Morse Regions: A Topological Approach to Local Feature Detection}, + year={2014}, + volume={23}, + number={12}, + pages={5612-5625}, + abstract={This paper introduces a topological approach to local invariant feature detection motivated by Morse theory. We use the critical points of the graph of the intensity image, revealing directly the topology information as initial interest points. Critical points are selected from what we call a tree-based shape-space. In particular, they are selected from both the connected components of the upper level sets of the image (the Max-tree) and those of the lower level sets (the Min-tree). They correspond to specific nodes on those two trees: 1) to the leaves (extrema) and 2) to the nodes having bifurcation (saddle points). We then associate to each critical point the largest region that contains it and is topologically equivalent in its tree. We call such largest regions the tree-based Morse regions (TBMRs). The TBMR can be seen as a variant of maximally stable extremal region (MSER), which are contrasted regions. Contrarily to MSER, TBMR relies only on topological information and thus fully inherit the invariance properties of the space of shapes (e.g., invariance to affine contrast changes and covariance to continuous transformations). In particular, TBMR extracts the regions independently of the contrast, which makes it truly contrast invariant. Furthermore, it is quasi-parameter free. TBMR extraction is fast, having the same complexity as MSER. Experimentally, TBMR achieves a repeatability on par with state-of-the-art methods, but obtains a significantly higher number of features. Both the accuracy and robustness of TBMR are demonstrated by applications to image registration and 3D reconstruction.}, + keywords={feature extraction;image reconstruction;image registration;trees (mathematics);tree-based Morse regions;topological approach;local invariant feature detection;Morse theory;intensity image;initial interest points;critical points;tree-based shape-space;upper level image sets;Max-tree;lower level sets;Min-tree;saddle points;bifurcation;maximally stable extremal region variant;MSER;topological information;TBMR extraction;3D reconstruction;image registration;Feature extraction;Detectors;Shape;Time complexity;Level set;Three-dimensional displays;Image registration;Min/Max tree;local features;affine region detectors;image registration;3D reconstruction;Min/Max tree;local features;affine region detectors;image registration;3D reconstruction}, + doi={10.1109/TIP.2014.2364127}, + ISSN={1941-0042}, + month={Dec},} + @article{Simonyan14, author = {Simonyan, K. and Vedaldi, A. and Zisserman, A.}, title = {Learning Local Feature Descriptors Using Convex Optimisation}, @@ -126,4 +140,4 @@ @incollection{LUCID pages = {1--9} year = {2012} publisher = {NIPS} -} +} \ No newline at end of file diff --git a/modules/xfeatures2d/include/opencv2/xfeatures2d.hpp b/modules/xfeatures2d/include/opencv2/xfeatures2d.hpp index b2eda6f643e..103dbe8e78c 100644 --- a/modules/xfeatures2d/include/opencv2/xfeatures2d.hpp +++ b/modules/xfeatures2d/include/opencv2/xfeatures2d.hpp @@ -901,7 +901,7 @@ class CV_EXPORTS_W HarrisLaplaceFeatureDetector : public Feature2D * The interface is equivalent to @ref Feature2D, adding operations for * @ref Elliptic_KeyPoint "Elliptic_KeyPoints" instead of @ref KeyPoint "KeyPoints". */ -class CV_EXPORTS AffineFeature2D : public Feature2D +class CV_EXPORTS_W AffineFeature2D : public Feature2D { public: /** @@ -945,6 +945,40 @@ class CV_EXPORTS AffineFeature2D : public Feature2D bool useProvidedKeypoints=false ) = 0; }; +/** +@brief Class implementing the Tree Based Morse Regions (TBMR) as described in +@cite Najman2014 extended with scaled extraction ability. + +@param min_area prune areas smaller than minArea +@param max_area_relative prune areas bigger than maxArea = max_area_relative * +input_image_size +@param scale_factor scale factor for scaled extraction. +@param n_scales number of applications of the scale factor (octaves). + +@note This algorithm is based on Component Tree (Min/Max) as well as MSER but +uses a Morse-theory approach to extract features. + +Features are ellipses (similar to MSER, however a MSER feature can never be a +TBMR feature and vice versa). + +*/ +class CV_EXPORTS_W TBMR : public AffineFeature2D +{ +public: + CV_WRAP static Ptr create(int min_area = 60, + float max_area_relative = 0.01f, + float scale_factor = 1.25f, + int n_scales = -1); + + CV_WRAP virtual void setMinArea(int minArea) = 0; + CV_WRAP virtual int getMinArea() const = 0; + CV_WRAP virtual void setMaxAreaRelative(float maxArea) = 0; + CV_WRAP virtual float getMaxAreaRelative() const = 0; + CV_WRAP virtual void setScaleFactor(float scale_factor) = 0; + CV_WRAP virtual float getScaleFactor() const = 0; + CV_WRAP virtual void setNScales(int n_scales) = 0; + CV_WRAP virtual int getNScales() const = 0; +}; /** @brief Estimates cornerness for prespecified KeyPoints using the FAST algorithm diff --git a/modules/xfeatures2d/src/msd.cpp b/modules/xfeatures2d/src/msd.cpp index 637d992cbc9..9fe5ae8cf0a 100644 --- a/modules/xfeatures2d/src/msd.cpp +++ b/modules/xfeatures2d/src/msd.cpp @@ -55,78 +55,14 @@ University of Bologna, Open Perception */ #include "precomp.hpp" +#include "msd_pyramid.hpp" #include namespace cv { namespace xfeatures2d { - /*! - MSD Image Pyramid. - */ - class MSDImagePyramid - { - // Multi-threaded construction of the scale-space pyramid - struct MSDImagePyramidBuilder : ParallelLoopBody - { - - MSDImagePyramidBuilder(const cv::Mat& _im, std::vector* _m_imPyr, float _scaleFactor) - { - im = &_im; - m_imPyr = _m_imPyr; - scaleFactor = _scaleFactor; - - } - - void operator()(const Range& range) const CV_OVERRIDE - { - for (int lvl = range.start; lvl < range.end; lvl++) - { - float scale = 1 / std::pow(scaleFactor, (float) lvl); - (*m_imPyr)[lvl] = cv::Mat(cv::Size(cvRound(im->cols * scale), cvRound(im->rows * scale)), im->type()); - cv::resize(*im, (*m_imPyr)[lvl], cv::Size((*m_imPyr)[lvl].cols, (*m_imPyr)[lvl].rows), 0.0, 0.0, cv::INTER_AREA); - } - } - const cv::Mat* im; - std::vector* m_imPyr; - float scaleFactor; - }; - - public: - MSDImagePyramid(const cv::Mat &im, const int nLevels, const float scaleFactor = 1.6f); - ~MSDImagePyramid(); - - const std::vector getImPyr() const - { - return m_imPyr; - }; - - private: - - std::vector m_imPyr; - int m_nLevels; - float m_scaleFactor; - }; - - MSDImagePyramid::MSDImagePyramid(const cv::Mat & im, const int nLevels, const float scaleFactor) - { - m_nLevels = nLevels; - m_scaleFactor = scaleFactor; - m_imPyr.clear(); - m_imPyr.resize(nLevels); - - m_imPyr[0] = im.clone(); - - if (m_nLevels > 1) - { - parallel_for_(Range(1, nLevels), MSDImagePyramidBuilder(im, &m_imPyr, scaleFactor)); - } - } - - MSDImagePyramid::~MSDImagePyramid() - { - } /*! MSD Implementation. diff --git a/modules/xfeatures2d/src/msd_pyramid.hpp b/modules/xfeatures2d/src/msd_pyramid.hpp new file mode 100644 index 00000000000..9fc3243a320 --- /dev/null +++ b/modules/xfeatures2d/src/msd_pyramid.hpp @@ -0,0 +1,77 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef __OPENCV_XFEATURES2D_MSD_PYRAMID_HPP__ +#define __OPENCV_XFEATURES2D_MSD_PYRAMID_HPP__ + +#include "precomp.hpp" + +namespace cv +{ +namespace xfeatures2d +{ +/*! + MSD Image Pyramid. + */ +class MSDImagePyramid +{ + // Multi-threaded construction of the scale-space pyramid + struct MSDImagePyramidBuilder : ParallelLoopBody + { + + MSDImagePyramidBuilder(const cv::Mat& _im, std::vector* _m_imPyr, float _scaleFactor) + { + im = &_im; + m_imPyr = _m_imPyr; + scaleFactor = _scaleFactor; + + } + + void operator()(const Range& range) const CV_OVERRIDE + { + for (int lvl = range.start; lvl < range.end; lvl++) + { + float scale = 1 / std::pow(scaleFactor, (float) lvl); + (*m_imPyr)[lvl] = cv::Mat(cv::Size(cvRound(im->cols * scale), cvRound(im->rows * scale)), im->type()); + cv::resize(*im, (*m_imPyr)[lvl], cv::Size((*m_imPyr)[lvl].cols, (*m_imPyr)[lvl].rows), 0.0, 0.0, cv::INTER_AREA); + } + } + const cv::Mat* im; + std::vector* m_imPyr; + float scaleFactor; + }; + +public: + + MSDImagePyramid(const cv::Mat &im, const int nLevels, const float scaleFactor = 1.6f) + { + m_nLevels = nLevels; + m_scaleFactor = scaleFactor; + m_imPyr.clear(); + m_imPyr.resize(nLevels); + + m_imPyr[0] = im.clone(); + + if (m_nLevels > 1) + { + parallel_for_(Range(1, nLevels), MSDImagePyramidBuilder(im, &m_imPyr, scaleFactor)); + } + } + ~MSDImagePyramid() {}; + + const std::vector getImPyr() const + { + return m_imPyr; + }; + +private: + + std::vector m_imPyr; + int m_nLevels; + float m_scaleFactor; +}; +} +} + +#endif \ No newline at end of file diff --git a/modules/xfeatures2d/src/tbmr.cpp b/modules/xfeatures2d/src/tbmr.cpp new file mode 100644 index 00000000000..36a0a538b7e --- /dev/null +++ b/modules/xfeatures2d/src/tbmr.cpp @@ -0,0 +1,584 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level +// directory of this distribution and at http://opencv.org/license.html. + +#include "precomp.hpp" +#include "msd_pyramid.hpp" + +namespace cv +{ +namespace xfeatures2d +{ + +class TBMR_Impl CV_FINAL : public TBMR +{ + public: + struct Params + { + Params(int _min_area = 60, float _max_area_relative = 0.01, + float _scale = 1.5, int _n_scale = -1) + { + CV_Assert(_min_area >= 0); + CV_Assert(_max_area_relative >= + std::numeric_limits::epsilon()); + + minArea = _min_area; + maxAreaRelative = _max_area_relative; + scale = _scale; + n_scale = _n_scale; + } + + uint minArea; + float maxAreaRelative; + int n_scale; + float scale; + }; + + explicit TBMR_Impl(const Params &_params) : params(_params) {} + + virtual ~TBMR_Impl() CV_OVERRIDE {} + + virtual void setMinArea(int minArea) CV_OVERRIDE + { + params.minArea = std::max(minArea, 0); + } + int getMinArea() const CV_OVERRIDE { return params.minArea; } + + virtual void setMaxAreaRelative(float maxAreaRelative) CV_OVERRIDE + { + params.maxAreaRelative = + std::max(maxAreaRelative, std::numeric_limits::epsilon()); + } + virtual float getMaxAreaRelative() const CV_OVERRIDE + { + return params.maxAreaRelative; + } + virtual void setScaleFactor(float scale_factor) CV_OVERRIDE + { + params.scale = std::max(scale_factor, 1.f); + } + virtual float getScaleFactor() const CV_OVERRIDE { return params.scale; } + virtual void setNScales(int n_scales) CV_OVERRIDE + { + params.n_scale = n_scales; + } + virtual int getNScales() const CV_OVERRIDE { return params.n_scale; } + + virtual void detect(InputArray image, + CV_OUT std::vector &keypoints, + InputArray mask = noArray()) CV_OVERRIDE; + + virtual void detect(InputArray image, + CV_OUT std::vector &keypoints, + InputArray mask = noArray()) CV_OVERRIDE; + + virtual void + detectAndCompute(InputArray image, InputArray mask, + CV_OUT std::vector &keypoints, + OutputArray descriptors, + bool useProvidedKeypoints = false) CV_OVERRIDE; + + CV_INLINE uint zfindroot(uint *parent, uint p) + { + if (parent[p] == p) + return p; + else + return parent[p] = zfindroot(parent, parent[p]); + } + + // Calculate the Component tree. Based on the order of S, it will be a + // min or max tree. + void calcMinMaxTree(Mat ima) + { + int rs = ima.rows; + int cs = ima.cols; + uint imSize = (uint)rs * cs; + + std::array offsets = { + -ima.cols, -1, 1, ima.cols + }; // {-1,0}, {0,-1}, {0,1}, {1,0} yx + std::array offsetsv = { Vec2i(0, -1), Vec2i(-1, 0), + Vec2i(1, 0), Vec2i(0, 1) }; // xy + AutoBuffer zparb(imSize); + AutoBuffer rootb(imSize); + AutoBuffer rankb(imSize); + memset(rankb.data(), 0, imSize * sizeof(uint)); + uint* zpar = zparb.data(); + uint *root = rootb.data(); + uint *rank = rankb.data(); + parent = Mat(rs, cs, CV_32S); // unsigned + AutoBuffer dejaVub(imSize); + memset(dejaVub.data(), 0, imSize * sizeof(bool)); + bool* dejaVu = dejaVub.data(); + + const uint *S_ptr = S.ptr(); + uint *parent_ptr = parent.ptr(); + Vec *imaAttribute = imaAttributes.ptr>(); + + for (int i = imSize - 1; i >= 0; --i) + { + uint p = S_ptr[i]; + + Vec2i idx_p(p % cs, p / cs); + // make set + { + parent_ptr[p] = p; + zpar[p] = p; + root[p] = p; + dejaVu[p] = true; + imaAttribute[p][0] = 1; // area + imaAttribute[p][1] = idx_p[0]; // sum_x + imaAttribute[p][2] = idx_p[1]; // sum_y + imaAttribute[p][3] = idx_p[0] * idx_p[1]; // sum_xy + imaAttribute[p][4] = idx_p[0] * idx_p[0]; // sum_xx + imaAttribute[p][5] = idx_p[1] * idx_p[1]; // sum_yy + } + + uint x = p; // zpar of p + for (unsigned k = 0; k < offsets.size(); ++k) + { + uint q = p + offsets[k]; + + Vec2i q_idx = idx_p + offsetsv[k]; + bool inBorder = q_idx[0] >= 0 && q_idx[0] < ima.cols && + q_idx[1] >= 0 && + q_idx[1] < ima.rows; // filter out border cases + + if (inBorder && dejaVu[q]) // remove first check + // obsolete + { + uint r = zfindroot(zpar, q); + if (r != x) // make union + { + parent_ptr[root[r]] = p; + // accumulate information + imaAttribute[p][0] += imaAttribute[root[r]][0]; // area + imaAttribute[p][1] += imaAttribute[root[r]][1]; // sum_x + imaAttribute[p][2] += imaAttribute[root[r]][2]; // sum_y + imaAttribute[p][3] += + imaAttribute[root[r]][3]; // sum_xy + imaAttribute[p][4] += + imaAttribute[root[r]][4]; // sum_xx + imaAttribute[p][5] += + imaAttribute[root[r]][5]; // sum_yy + + if (rank[x] < rank[r]) + { + // we merge p to r + zpar[x] = r; + root[r] = p; + x = r; + } + else if (rank[r] < rank[p]) + { + // merge r to p + zpar[r] = p; + } + else + { + // same height + zpar[r] = p; + rank[p] += 1; + } + } + } + } + } + } + + void calculateTBMRs(const Mat &image, std::vector &tbmrs, + const Mat &mask, float scale, int octave) + { + uint imSize = image.cols * image.rows; + uint maxArea = + static_cast(params.maxAreaRelative * imSize * scale); + uint minArea = static_cast(params.minArea * scale); + + if (parent.empty() || parent.size != image.size) + parent = Mat(image.rows, image.cols, CV_32S); + + if (imaAttributes.empty() || imaAttributes.size != image.size) + imaAttributes = Mat(image.rows, image.cols, CV_32SC(6)); + + calcMinMaxTree(image); + + const Vec *imaAttribute = + imaAttributes.ptr>(); + const uint8_t *ima_ptr = image.ptr(); + const uint *S_ptr = S.ptr(); + uint *parent_ptr = parent.ptr(); + + // canonization + for (uint i = 0; i < imSize; ++i) + { + uint p = S_ptr[i]; + uint q = parent_ptr[p]; + if (ima_ptr[parent_ptr[q]] == ima_ptr[q]) + parent_ptr[p] = parent_ptr[q]; + } + + // TBMRs extraction + //------------------------------------------------------------------------ + // small variant of the given algorithm in the paper. For each + // critical node having more than one child, we check if the + // largest region containing this node without any change of + // topology is above its parent, if not, discard this critical + // node. + // + // note also that we do not select the critical nodes themselves + // as final TBMRs + //-------------------------------------------------------------------------- + + AutoBuffer numSonsb(imSize); + memset(numSonsb.data(), 0, imSize * sizeof(uint)); + uint* numSons = numSonsb.data(); + uint vecNodesSize = imaAttribute[S_ptr[0]][0]; // area + AutoBuffer vecNodesb(vecNodesSize); + memset(vecNodesb.data(), 0, vecNodesSize * sizeof(uint)); + uint *vecNodes = vecNodesb.data(); // area + uint numNodes = 0; + + // leaf to root propagation to select the canonized nodes + for (int i = imSize - 1; i >= 0; --i) + { + uint p = S_ptr[i]; + if (parent_ptr[p] == p || ima_ptr[p] != ima_ptr[parent_ptr[p]]) + { + vecNodes[numNodes++] = p; + if (imaAttribute[p][0] >= minArea) // area + numSons[parent_ptr[p]]++; + } + } + + AutoBuffer isSeenb(imSize); + memset(isSeenb.data(), 0, imSize * sizeof(bool)); + bool *isSeen = isSeenb.data(); + + // parent of critical leaf node + AutoBuffer isParentofLeafb(imSize); + memset(isParentofLeafb.data(), 0, imSize * sizeof(bool)); + bool* isParentofLeaf = isParentofLeafb.data(); + + for (uint i = 0; i < vecNodesSize; i++) + { + uint p = vecNodes[i]; + if (numSons[p] == 0 && numSons[parent_ptr[p]] == 1) + isParentofLeaf[parent_ptr[p]] = true; + } + + uint numTbmrs = 0; + AutoBuffer vecTbmrsb(numNodes); + uint* vecTbmrs = vecTbmrsb.data(); + for (uint i = 0; i < vecNodesSize; i++) + { + uint p = vecNodes[i]; + if (numSons[p] == 1 && !isSeen[p] && imaAttribute[p][0] <= maxArea) + { + uint num_ancestors = 0; + uint pt = p; + uint po = pt; + while (numSons[pt] == 1 && imaAttribute[pt][0] <= maxArea) + { + isSeen[pt] = true; + num_ancestors++; + po = pt; + pt = parent_ptr[pt]; + } + if (!isParentofLeaf[p] || num_ancestors > 1) + { + vecTbmrs[numTbmrs++] = po; + } + } + } + // end of TBMRs extraction + //------------------------------------------------------------------------ + + // compute best fitting ellipses + //------------------------------------------------------------------------ + for (uint i = 0; i < numTbmrs; i++) + { + uint p = vecTbmrs[i]; + double area = static_cast(imaAttribute[p][0]); + double sum_x = static_cast(imaAttribute[p][1]); + double sum_y = static_cast(imaAttribute[p][2]); + double sum_xy = static_cast(imaAttribute[p][3]); + double sum_xx = static_cast(imaAttribute[p][4]); + double sum_yy = static_cast(imaAttribute[p][5]); + + // Barycenter: + double x = sum_x / area; + double y = sum_y / area; + + double i20 = sum_xx - area * x * x; + double i02 = sum_yy - area * y * y; + double i11 = sum_xy - area * x * y; + double n = i20 * i02 - i11 * i11; + if (n != 0) + { + double a = (i02 / n) * (area - 1) / 4; + double b = (-i11 / n) * (area - 1) / 4; + double c = (i20 / n) * (area - 1) / 4; + + // filter out some non meaningful ellipses + double a1 = a; + double b1 = b; + double c1 = c; + uint ai = 0; + uint bi = 0; + uint ci = 0; + if (a > 0) + { + if (a < 0.00005) + a1 = 0; + else if (a < 0.0001) + { + a1 = 0.0001; + } + else + { + ai = (uint)(10000 * a); + a1 = (double)ai / 10000; + } + } + else + { + if (a > -0.00005) + a1 = 0; + else if (a > -0.0001) + a1 = -0.0001; + else + { + ai = (uint)(10000 * (-a)); + a1 = -(double)ai / 10000; + } + } + + if (b > 0) + { + if (b < 0.00005) + b1 = 0; + else if (b < 0.0001) + { + b1 = 0.0001; + } + else + { + bi = (uint)(10000 * b); + b1 = (double)bi / 10000; + } + } + else + { + if (b > -0.00005) + b1 = 0; + else if (b > -0.0001) + b1 = -0.0001; + else + { + bi = (uint)(10000 * (-b)); + b1 = -(double)bi / 10000; + } + } + + if (c > 0) + { + if (c < 0.00005) + c1 = 0; + else if (c < 0.0001) + { + c1 = 0.0001; + } + else + { + ci = (uint)(10000 * c); + c1 = (double)ci / 10000; + } + } + else + { + if (c > -0.00005) + c1 = 0; + else if (c > -0.0001) + c1 = -0.0001; + else + { + ci = (uint)(10000 * (-c)); + c1 = -(double)ci / 10000; + } + } + double v = + (a1 + c1 - + std::sqrt(a1 * a1 + c1 * c1 + 4 * b1 * b1 - 2 * a1 * c1)) / + 2; + + double l1 = 1. / std::sqrt((a + c + + std::sqrt(a * a + c * c + + 4 * b * b - 2 * a * c)) / + 2); + double l2 = 1. / std::sqrt((a + c - + std::sqrt(a * a + c * c + + 4 * b * b - 2 * a * c)) / + 2); + double minAxL = std::min(l1, l2); + double majAxL = std::max(l1, l2); + + if (minAxL >= 1.5 && v != 0 && + (mask.empty() || + mask.at(cvRound(y), cvRound(x)) != 0)) + { + double theta = 0; + if (b == 0) + if (a < c) + theta = 0; + else + theta = CV_PI / 2.; + else + theta = CV_PI / 2. + 0.5 * std::atan2(2 * b, (a - c)); + + float size = (float)majAxL; + + // not sure if we should scale or not scale x,y,axes,size + // (as scale is stored in si) + Elliptic_KeyPoint ekp( + Point2f((float)x, (float)y) * scale, (float)theta, + cv::Size2f((float)majAxL, (float)minAxL) * scale, + size * scale, scale); + ekp.octave = octave; + tbmrs.push_back(ekp); + } + } + } + //--------------------------------------------- + } + + Mat tempsrc; + + // component tree representation (parent,S): see + // https://ieeexplore.ieee.org/document/6850018 + Mat parent; + Mat S; + // moments: compound type of: (area, x, y, xy, xx, yy) + Mat imaAttributes; + + Params params; +}; + +void TBMR_Impl::detect(InputArray _image, std::vector &keypoints, + InputArray _mask) +{ + std::vector kp; + detect(_image, kp, _mask); + keypoints.resize(kp.size()); + for (size_t i = 0; i < kp.size(); ++i) + keypoints[i] = kp[i]; +} + +void TBMR_Impl::detect(InputArray _image, + std::vector &keypoints, + InputArray _mask) +{ + Mat mask = _mask.getMat(); + Mat src = _image.getMat(); + + keypoints.clear(); + + if (src.empty()) + return; + + if (!mask.empty()) + { + CV_Assert(mask.type() == CV_8UC1); + CV_Assert(mask.size == src.size); + } + + if (!src.isContinuous()) + { + src.copyTo(tempsrc); + src = tempsrc; + } + + CV_Assert(src.depth() == CV_8U); + + if (src.channels() != 1) + cv::cvtColor(src, src, cv::COLOR_BGR2GRAY); + + int m_cur_n_scales = + params.n_scale > 0 + ? params.n_scale + : 1 /*todo calculate optimal scale factor from image size*/; + float m_scale_factor = params.scale; + + // track and eliminate duplicates introduced with multi scale position -> + // (size) + Mat dupl(src.rows / 4, src.cols / 4, CV_32F, cv::Scalar::all(0)); + float *dupl_ptr = dupl.ptr(); + + std::vector pyr; + MSDImagePyramid scaleSpacer(src, m_cur_n_scales, m_scale_factor); + pyr = scaleSpacer.getImPyr(); + + int oct = 0; + for (auto &s : pyr) + { + float scale = ((float)s.cols) / pyr.begin()->cols; + std::vector kpts; + + // append max tree tbmrs + sortIdx(s.reshape(1, 1), S, + SortFlags::SORT_ASCENDING | SortFlags::SORT_EVERY_ROW); + calculateTBMRs(s, kpts, mask, scale, oct); + + // reverse instead of sort + flip(S, S, -1); + calculateTBMRs(s, kpts, mask, scale, oct); + + if (oct == 0) + { + for (const auto &k : kpts) + { + dupl_ptr[(int)(k.pt.x / 4) + + (int)(k.pt.y / 4) * (src.cols / 4)] = k.size; + } + keypoints.insert(keypoints.end(), kpts.begin(), kpts.end()); + } + else + { + for (const auto &k : kpts) + { + float &sz = dupl_ptr[(int)(k.pt.x / 4) + + (int)(k.pt.y / 4) * (src.cols / 4)]; + // we hereby add only features that are at least 4 pixels away + // or have a significantly different size + if (std::abs(k.size - sz) / std::max(k.size, sz) >= 0.2f) + { + sz = k.size; + keypoints.push_back(k); + } + } + } + + oct++; + } +} + +void TBMR_Impl::detectAndCompute( + InputArray image, InputArray mask, + CV_OUT std::vector &keypoints, OutputArray descriptors, + bool useProvidedKeypoints) +{ + // We can use SIFT to compute descriptors for the extracted keypoints... + auto sift = SIFT::create(); + auto dac = AffineFeature2D::create(this, sift); + dac->detectAndCompute(image, mask, keypoints, descriptors, + useProvidedKeypoints); +} + +Ptr TBMR::create(int _min_area, float _max_area_relative, float _scale, + int _n_scale) +{ + return cv::makePtr( + TBMR_Impl::Params(_min_area, _max_area_relative, _scale, _n_scale)); +} + +} // namespace xfeatures2d +} // namespace cv \ No newline at end of file diff --git a/modules/xfeatures2d/test/test_features2d.cpp b/modules/xfeatures2d/test/test_features2d.cpp index 6a8d4b5db89..e79e50370bb 100644 --- a/modules/xfeatures2d/test/test_features2d.cpp +++ b/modules/xfeatures2d/test/test_features2d.cpp @@ -85,6 +85,12 @@ TEST( Features2d_Detector_Harris_Laplace_Affine, regression ) test.safe_run(); } +TEST(Features2d_Detector_TBMR_Affine, regression) +{ + CV_FeatureDetectorTest test("detector-tbmr-affine", TBMR::create()); + test.safe_run(); +} + /* * Descriptors */ diff --git a/modules/xfeatures2d/test/test_keypoints.cpp b/modules/xfeatures2d/test/test_keypoints.cpp index 45d50dfb4b6..28d125d0836 100644 --- a/modules/xfeatures2d/test/test_keypoints.cpp +++ b/modules/xfeatures2d/test/test_keypoints.cpp @@ -137,4 +137,10 @@ TEST(Features2d_Detector_Keypoints_MSDDetector, validation) test.safe_run(); } +TEST(Features2d_Detector_Keypoints_TBMRDetector, validation) +{ + CV_FeatureDetectorKeypointsTest test(xfeatures2d::TBMR::create()); + test.safe_run(); +} + }} // namespace From 2bf643483eab3df7d0964907416cbf3446287145 Mon Sep 17 00:00:00 2001 From: Zhiju Cen Date: Fri, 20 Nov 2020 16:22:59 +0800 Subject: [PATCH 25/29] VS compatibility with unicode strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UTF-8 string u8"…" causes following errors when building under VS2019: C2001 newline in constant C2143 syntax error: missing ';' before '}' C2146 syntax error: missing ';' before identifier 'str' --- modules/cvv/src/stfl/stringutils.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/cvv/src/stfl/stringutils.cpp b/modules/cvv/src/stfl/stringutils.cpp index 3ce0f145747..71d07d9bb67 100644 --- a/modules/cvv/src/stfl/stringutils.cpp +++ b/modules/cvv/src/stfl/stringutils.cpp @@ -248,15 +248,16 @@ void unescapeCommas(QString &str) QString shortenString(QString &str, int maxLength, bool cutEnd, bool fill) { + const auto horizontalEllipsis = u8"\xE2\x80\xA6"; // u8"…" if (str.size() > maxLength) { if (cutEnd) { - str = str.mid(0, maxLength - 1) + u8"…"; + str = str.mid(0, maxLength - 1) + horizontalEllipsis; } else { - str = u8"…" + + str = horizontalEllipsis + str.mid(str.size() + 1 - maxLength, str.size()); } } From 478cc124f5934e07a2d69754b6b5a6013dbd0cff Mon Sep 17 00:00:00 2001 From: riskiest <33191846+riskiest@users.noreply.github.com> Date: Mon, 23 Nov 2020 20:39:32 +0800 Subject: [PATCH 26/29] Merge pull request #2671 from riskiest:color-calibration Color Calibration Algorithm Implementation Revised * Add utils, io helpers, the operations for linearization and distance * Add the code for color, colorspace, linearization and ccm computation * Add sample code for color correction * Add the dependency to opencv_imgcodes in CMakeLists.txt * Add the color correction tutorial, introducing build steps and parameters * Add sample code to color correction tutorial * Add color correction algorithms introductions * Update color_correction_model.markdown * Fix warnings of whitespace, undeclared function, shadow variables. * Fix the warnings of shadow variables, unused variable in base class. Fix the error whitespace and 'EOF' on the docs. * Fix the warnnings on win & macos * Fix bugs & support Vinyl ColorChecker * fix shadow variables warning & code style * update document for sample * update license * fix linearize.hpp * Add basic io, utils, operations helpers. Implement color distance. * Implement color, colorspace, linearization and ccm features. * Add the dependencies to opencv_imgcodecs in CMakeLists.txt * Add color correction model sample code. Co-authored-by: Chenqi Shan * Add the index markdown of color correction tutorial. Co-authored-by: Chenqi Shan * Add the introduction for color correction sample. * Split operations into .hpp and .cpp * Split mcc, color, colorspace and linearize into .cpp & .hpp * Update test cases * Split distance, io and utils into cpp & hpp. Refer ccm.hpp in entrypoint header and update realted refs in sampe & tutorial * add static method * fix shared_ptr * fix markdown for new version * delete useless include message * update unittests * update docs & fix bugs for InitialwhiteBalance() * update doc for doxygen * update doc&DigitalSG * replace whitespace for utils.hpp&color.hpp * update getilluminants,imgcodes, * Fix Mat wrapper over data from C arrays, fix doxygen's @snippet instead of direct code. * remove array from color.h * remove hpp from include/mcc/ * add hpp to opencv/model/mcc/ * dst unsolved * remove bugs about dst * add make passed * update codes using the structure "impl" * update documents * update ccm member for class ColorCorrectionModel * remove macro CV_EXPORTS_W for codes in src/*.hpp * move class Impl private * remove unnesasary notice * remove trailing whitespace * update documents&samples * move typedef MatFunc into class and move dead codes * minimize list of required headers, add getCCM() method * move type: information for parameters * move underscores _ in public headers * add @defgroup for ccm * move and add getloss() method for class ColorCorrection Model * update sample/color_correction_model.cpp * add getIOs() function for minimize initialization of IO variables * mcc(ccm): apply clang-format * mcc(ccm): fix documentation, code style * remove duplicate enum values * add prefixes for enum values * update codes using cv_Error * update test_ccm file * update test_ccm file * update sample --help * mcc: reduce global initializers * update function naming style * update formulas and note for ccm.hpp * add const value Co-authored-by: Chenqi Shan Co-authored-by: Jinheng Zhang Co-authored-by: Zhen Ju Co-authored-by: Longbu Wang Co-authored-by: shanchenqi <582533558@qq.com> --- modules/mcc/CMakeLists.txt | 9 +- modules/mcc/include/opencv2/mcc.hpp | 8 +- modules/mcc/include/opencv2/mcc/ccm.hpp | 519 ++++++++++++ .../mcc/samples/color_correction_model.cpp | 158 ++++ modules/mcc/src/ccm.cpp | 438 ++++++++++ modules/mcc/src/color.cpp | 398 +++++++++ modules/mcc/src/color.hpp | 118 +++ modules/mcc/src/colorspace.cpp | 789 ++++++++++++++++++ modules/mcc/src/colorspace.hpp | 365 ++++++++ modules/mcc/src/distance.cpp | 222 +++++ modules/mcc/src/distance.hpp | 99 +++ modules/mcc/src/io.cpp | 133 +++ modules/mcc/src/io.hpp | 72 ++ modules/mcc/src/linearize.cpp | 130 +++ modules/mcc/src/linearize.hpp | 209 +++++ modules/mcc/src/operations.cpp | 90 ++ modules/mcc/src/operations.hpp | 102 +++ modules/mcc/src/utils.cpp | 119 +++ modules/mcc/src/utils.hpp | 155 ++++ modules/mcc/test/test_ccm.cpp | 166 ++++ modules/mcc/test/test_precomp.hpp | 3 + .../basic_ccm/color_correction_model.markdown | 122 +++ .../tutorials/table_of_content_ccm.markdown | 8 + 23 files changed, 4430 insertions(+), 2 deletions(-) create mode 100644 modules/mcc/include/opencv2/mcc/ccm.hpp create mode 100644 modules/mcc/samples/color_correction_model.cpp create mode 100644 modules/mcc/src/ccm.cpp create mode 100644 modules/mcc/src/color.cpp create mode 100644 modules/mcc/src/color.hpp create mode 100644 modules/mcc/src/colorspace.cpp create mode 100644 modules/mcc/src/colorspace.hpp create mode 100644 modules/mcc/src/distance.cpp create mode 100644 modules/mcc/src/distance.hpp create mode 100644 modules/mcc/src/io.cpp create mode 100644 modules/mcc/src/io.hpp create mode 100644 modules/mcc/src/linearize.cpp create mode 100644 modules/mcc/src/linearize.hpp create mode 100644 modules/mcc/src/operations.cpp create mode 100644 modules/mcc/src/operations.hpp create mode 100644 modules/mcc/src/utils.cpp create mode 100644 modules/mcc/src/utils.hpp create mode 100644 modules/mcc/test/test_ccm.cpp create mode 100644 modules/mcc/tutorials/basic_ccm/color_correction_model.markdown create mode 100644 modules/mcc/tutorials/table_of_content_ccm.markdown diff --git a/modules/mcc/CMakeLists.txt b/modules/mcc/CMakeLists.txt index 806303505f3..2b7fadbaa14 100644 --- a/modules/mcc/CMakeLists.txt +++ b/modules/mcc/CMakeLists.txt @@ -1,2 +1,9 @@ set(the_description "Macbeth Chart Detection") -ocv_define_module(mcc opencv_core opencv_imgproc opencv_calib3d opencv_photo opencv_dnn WRAP python) +ocv_define_module(mcc + opencv_core + opencv_imgproc + opencv_calib3d + opencv_dnn + WRAP + python +) diff --git a/modules/mcc/include/opencv2/mcc.hpp b/modules/mcc/include/opencv2/mcc.hpp index 2027485fb2a..e49f3053ffa 100644 --- a/modules/mcc/include/opencv2/mcc.hpp +++ b/modules/mcc/include/opencv2/mcc.hpp @@ -32,8 +32,15 @@ #include "mcc/checker_detector.hpp" #include "mcc/checker_model.hpp" +#include "mcc/ccm.hpp" /** @defgroup mcc Macbeth Chart module +@{ + @defgroup color_correction Color Correction Model +@} + + +@addtogroup mcc Introduction ------------ @@ -47,7 +54,6 @@ colors that are present in the image, based on this information we can apply any suitable algorithm to find the actual color of all the objects present in the image. - */ #endif diff --git a/modules/mcc/include/opencv2/mcc/ccm.hpp b/modules/mcc/include/opencv2/mcc/ccm.hpp new file mode 100644 index 00000000000..a3686db473d --- /dev/null +++ b/modules/mcc/include/opencv2/mcc/ccm.hpp @@ -0,0 +1,519 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_CCM_HPP__ +#define __OPENCV_MCC_CCM_HPP__ + +#include +#include + +namespace cv +{ +namespace ccm +{ +/** @addtogroup color_correction +@{ + +Introduction +------------ + +The purpose of color correction is to adjust the color response of input +and output devices to a known state. The device being calibrated is sometimes +called the calibration source; the color space used as the standard is sometimes +called the calibration target. Color calibration has been used in many industries, +such as television production, games, photography, engineering, chemistry, +medicine, etc. Due to the manufacturing process of the input and output equipment, +the channel response has nonlinear distortion. In order to correct the picture output +of the equipment, it is nessary to calibrate the captured color and the actual color. + +*/ + + + +/** @brief Enum of the possible types of ccm. +*/ +enum CCM_TYPE +{ + CCM_3x3, ///< The CCM with the shape \f$3\times3\f$ performs linear transformation on color values. + CCM_4x3, ///< The CCM with the shape \f$4\times3\f$ performs affine transformation. +}; + +/** @brief Enum of the possible types of initial method. +*/ +enum INITIAL_METHOD_TYPE +{ + INITIAL_METHOD_WHITE_BALANCE, ///< The white balance method. The initial value is:\n + /// \f$ + /// M_{CCM}= + /// \begin{bmatrix} + /// k_R & 0 & 0\\ + /// 0 & k_G & 0\\ + /// 0 & 0 & k_B\\ + /// \end{bmatrix} + /// \f$\n + /// where\n + /// \f$ + /// k_R=mean(R_{li}')/mean(R_{li})\\ + /// k_R=mean(G_{li}')/mean(G_{li})\\ + /// k_R=mean(B_{li}')/mean(B_{li}) + /// \f$ + INITIAL_METHOD_LEAST_SQUARE, ///0\\ +C_{sl}=0, \qquad C_s=0 +\f] + +Because \f$exp(ln(0))\to\infty \f$, the channel whose component is 0 is directly mapped to 0 in the formula above. + +For fitting channels respectively, we have: +\f[ +r=polyfit(ln(R_s),ln(R_{dl}))\\ +g=polyfit(ln(G_s),ln(G_{dl}))\\ +b=polyfit(ln(B_s),ln(B_{dl}))\\ +\f] +Note that the parameter of \f$ln(*) \f$ cannot be 0. +Therefore, we need to delete the channels whose values are 0 from \f$R_s \f$ and \f$R_{dl} \f$, \f$G_s\f$ and \f$G_{dl}\f$, \f$B_s\f$ and \f$B_{dl}\f$. + +Therefore: + +\f[ +ln(R_{sl})=r(ln(R_s)), \qquad R_s>0\\ +R_{sl}=0, \qquad R_s=0\\ +ln(G_{sl})=g(ln(G_s)),\qquad G_s>0\\ +G_{sl}=0, \qquad G_s=0\\ +ln(B_{sl})=b(ln(B_s)),\qquad B_s>0\\ +B_{sl}=0, \qquad B_s=0\\ +\f] + +For grayscale polynomials, there are also: +\f[ +f=polyfit(ln(G_{sl}),ln(G_{dl})) +\f] +and: +\f[ +ln(C_{sl})=f(ln(C_s)), \qquad C_s>0\\ +C_sl=0, \qquad C_s=0 +\f] +*/ +enum LINEAR_TYPE +{ + + LINEARIZATION_IDENTITY, /// p; +}; + +//! @} ccm +} // namespace ccm +} // namespace cv + +#endif \ No newline at end of file diff --git a/modules/mcc/samples/color_correction_model.cpp b/modules/mcc/samples/color_correction_model.cpp new file mode 100644 index 00000000000..20df704370e --- /dev/null +++ b/modules/mcc/samples/color_correction_model.cpp @@ -0,0 +1,158 @@ +//! [tutorial] +#include + +#include +#include +#include +#include + +using namespace std; +using namespace cv; +using namespace mcc; +using namespace ccm; +using namespace std; + +const char *about = "Basic chart detection"; +const char *keys = + "{ help h | | show this message }" + "{t | | chartType: 0-Standard, 1-DigitalSG, 2-Vinyl }" + "{v | | Input from video file, if ommited, input comes from camera }" + "{ci | 0 | Camera id if input doesnt come from video (-v) }" + "{f | 1 | Path of the file to process (-v) }" + "{nc | 1 | Maximum number of charts in the image }"; + +int main(int argc, char *argv[]) +{ + + + // ---------------------------------------------------------- + // Scroll down a bit (~40 lines) to find actual relevant code + // ---------------------------------------------------------- + //! [get_messages_of_image] + CommandLineParser parser(argc, argv, keys); + parser.about(about); + if (argc==1 || parser.has("help")) + { + parser.printMessage(); + return 0; + } + + int t = parser.get("t"); + int nc = parser.get("nc"); + string filepath = parser.get("f"); + + CV_Assert(0 <= t && t <= 2); + TYPECHART chartType = TYPECHART(t); + + + if (!parser.check()) + { + parser.printErrors(); + return 0; + } + + Mat image = imread(filepath, IMREAD_COLOR); + if (!image.data) + { + cout << "Invalid Image!" << endl; + return 1; + } + //! [get_messages_of_image] + + Mat imageCopy = image.clone(); + Ptr detector = CCheckerDetector::create(); + // Marker type to detect + if (!detector->process(image, chartType, nc)) + { + printf("ChartColor not detected \n"); + return 2; + } + //! [get_color_checker] + vector> checkers = detector->getListColorChecker(); + //! [get_color_checker] + for (Ptr checker : checkers) + { + //! [create] + Ptr cdraw = CCheckerDraw::create(checker); + cdraw->draw(image); + Mat chartsRGB = checker->getChartsRGB(); + Mat src = chartsRGB.col(1).clone().reshape(3, chartsRGB.rows/3); + src /= 255.0; + //! [create] + + //compte color correction matrix + //! [get_ccm_Matrix] + ColorCorrectionModel model1(src, COLORCHECKER_Vinyl); + model1.run(); + Mat ccm = model1.getCCM(); + std::cout<<"ccm "<(18, 1) << + // Vec3d(100, 0.00520000001, -0.0104), + // Vec3d(73.0833969, -0.819999993, -2.02099991), + // Vec3d(62.493, 0.425999999, -2.23099995), + // Vec3d(50.4640007, 0.446999997, -2.32399988), + // Vec3d(37.7970009, 0.0359999985, -1.29700005), + // Vec3d(0, 0, 0), + // Vec3d(51.5880013, 73.5179977, 51.5690002), + // Vec3d(93.6989975, -15.7340002, 91.9420013), + // Vec3d(69.4079971, -46.5940018, 50.4869995), + // Vec3d(66.61000060000001, -13.6789999, -43.1720009), + // Vec3d(11.7110004, 16.9799995, -37.1759987), + // Vec3d(51.973999, 81.9440002, -8.40699959), + // Vec3d(40.5489998, 50.4399986, 24.8490009), + // Vec3d(60.8160019, 26.0690002, 49.4420013), + // Vec3d(52.2529984, -19.9500008, -23.9960003), + // Vec3d(51.2859993, 48.4700012, -15.0579996), + // Vec3d(68.70700069999999, 12.2959995, 16.2129993), + // Vec3d(63.6839981, 10.2930002, 16.7639999)); + + // ColorCorrectionModel model8(src,ref,COLOR_SPACE_Lab_D50_2); + // model8.run(); + //! [reference_color_values] + + //! [make_color_correction] + Mat img_; + cvtColor(image, img_, COLOR_BGR2RGB); + img_.convertTo(img_, CV_64F); + const int inp_size = 255; + const int out_size = 255; + img_ = img_ / inp_size; + Mat calibratedImage= model1.infer(img_); + Mat out_ = calibratedImage * out_size; + //! [make_color_correction] + + //! [Save_calibrated_image] + // Save the calibrated image to {FILE_NAME}.calibrated.{FILE_EXT} + out_.convertTo(out_, CV_8UC3); + Mat img_out = min(max(out_, 0), out_size); + Mat out_img; + cvtColor(img_out, out_img, COLOR_RGB2BGR); + string filename = filepath.substr(filepath.find_last_of('/')+1); + size_t dotIndex = filename.find_last_of('.'); + string baseName = filename.substr(0, dotIndex); + string ext = filename.substr(dotIndex+1, filename.length()-dotIndex); + string calibratedFilePath = baseName + ".calibrated." + ext; + imwrite(calibratedFilePath, out_img); + //! [Save_calibrated_image] + + } + + return 0; +} +//! [tutorial] \ No newline at end of file diff --git a/modules/mcc/src/ccm.cpp b/modules/mcc/src/ccm.cpp new file mode 100644 index 00000000000..7e26d164124 --- /dev/null +++ b/modules/mcc/src/ccm.cpp @@ -0,0 +1,438 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "opencv2/mcc/ccm.hpp" +#include "linearize.hpp" +namespace cv { +namespace ccm { +class ColorCorrectionModel::Impl +{ +public: + Mat src; + std::shared_ptr dst = std::make_shared(); + Mat dist; + RGBBase_& cs; + Mat mask; + + // RGBl of detected data and the reference + Mat src_rgbl; + Mat dst_rgbl; + + // ccm type and shape + CCM_TYPE ccm_type; + int shape; + + // linear method and distance + std::shared_ptr linear = std::make_shared(); + DISTANCE_TYPE distance; + LINEAR_TYPE linear_type; + + Mat weights; + Mat weights_list; + Mat ccm; + Mat ccm0; + double gamma; + int deg; + std::vector saturated_threshold; + INITIAL_METHOD_TYPE initial_method_type; + double weights_coeff; + int masked_len; + double loss; + int max_count; + double epsilon; + Impl(); + + /** @brief Make no change for CCM_3x3. + convert cv::Mat A to [A, 1] in CCM_4x3. + @param inp the input array, type of cv::Mat. + @return the output array, type of cv::Mat + */ + Mat prepare(const Mat& inp); + + /** @brief Calculate weights and mask. + @param weights_list the input array, type of cv::Mat. + @param weights_coeff type of double. + @param saturate_mask the input array, type of cv::Mat. + */ + void calWeightsMasks(const Mat& weights_list, double weights_coeff, Mat saturate_mask); + + /** @brief Fitting nonlinear - optimization initial value by white balance. + @return the output array, type of Mat + */ + void initialWhiteBalance(void); + + /** @brief Fitting nonlinear-optimization initial value by least square. + @param fit if fit is True, return optimalization for rgbl distance function. + */ + void initialLeastSquare(bool fit = false); + + double calc_loss_(Color color); + double calc_loss(const Mat ccm_); + + /** @brief Fitting ccm if distance function is associated with CIE Lab color space. + see details in https://github.com/opencv/opencv/blob/master/modules/core/include/opencv2/core/optim.hpp + Set terminal criteria for solver is possible. + */ + void fitting(void); + + void getColor(Mat& img_, bool islinear = false); + void getColor(CONST_COLOR constcolor); + void getColor(Mat colors_, COLOR_SPACE cs_, Mat colored_); + void getColor(Mat colors_, COLOR_SPACE ref_cs_); + + /** @brief Loss function base on cv::MinProblemSolver::Function. + see details in https://github.com/opencv/opencv/blob/master/modules/core/include/opencv2/core/optim.hpp + */ + class LossFunction : public MinProblemSolver::Function + { + public: + ColorCorrectionModel::Impl* ccm_loss; + LossFunction(ColorCorrectionModel::Impl* ccm) + : ccm_loss(ccm) {}; + + /** @brief Reset dims to ccm->shape. + */ + int getDims() const CV_OVERRIDE + { + return ccm_loss->shape; + } + + /** @brief Reset calculation. + */ + double calc(const double* x) const CV_OVERRIDE + { + Mat ccm_(ccm_loss->shape, 1, CV_64F); + for (int i = 0; i < ccm_loss->shape; i++) + { + ccm_.at(i, 0) = x[i]; + } + ccm_ = ccm_.reshape(0, ccm_loss->shape / 3); + return ccm_loss->calc_loss(ccm_); + } + }; +}; + +ColorCorrectionModel::Impl::Impl() + : cs(*GetCS::getInstance().get_rgb(COLOR_SPACE_sRGB)) + , ccm_type(CCM_3x3) + , distance(DISTANCE_CIE2000) + , linear_type(LINEARIZATION_GAMMA) + , weights(Mat()) + , gamma(2.2) + , deg(3) + , saturated_threshold({ 0, 0.98 }) + , initial_method_type(INITIAL_METHOD_LEAST_SQUARE) + , weights_coeff(0) + , max_count(5000) + , epsilon(1.e-4) +{} + +Mat ColorCorrectionModel::Impl::prepare(const Mat& inp) +{ + switch (ccm_type) + { + case cv::ccm::CCM_3x3: + shape = 9; + return inp; + case cv::ccm::CCM_4x3: + { + shape = 12; + Mat arr1 = Mat::ones(inp.size(), CV_64F); + Mat arr_out(inp.size(), CV_64FC4); + Mat arr_channels[3]; + split(inp, arr_channels); + merge(std::vector { arr_channels[0], arr_channels[1], arr_channels[2], arr1 }, arr_out); + return arr_out; + } + default: + CV_Error(Error::StsBadArg, "Wrong ccm_type!"); + break; + } +} + +void ColorCorrectionModel::Impl::calWeightsMasks(const Mat& weights_list_, double weights_coeff_, Mat saturate_mask) +{ + // weights + if (!weights_list_.empty()) + { + weights = weights_list_; + } + else if (weights_coeff_ != 0) + { + pow(dst->toLuminant(cs.io), weights_coeff_, weights); + } + + // masks + Mat weight_mask = Mat::ones(src.rows, 1, CV_8U); + if (!weights.empty()) + { + weight_mask = weights > 0; + } + this->mask = (weight_mask) & (saturate_mask); + + // weights' mask + if (!weights.empty()) + { + Mat weights_masked = maskCopyTo(this->weights, this->mask); + weights = weights_masked / mean(weights_masked)[0]; + } + masked_len = (int)sum(mask)[0]; +} + +void ColorCorrectionModel::Impl::initialWhiteBalance(void) +{ + Mat schannels[4]; + split(src_rgbl, schannels); + Mat dchannels[4]; + split(dst_rgbl, dchannels); + std::vector initial_vec = { sum(dchannels[0])[0] / sum(schannels[0])[0], 0, 0, 0, + sum(dchannels[1])[0] / sum(schannels[1])[0], 0, 0, 0, + sum(dchannels[2])[0] / sum(schannels[2])[0], 0, 0, 0 }; + std::vector initial_vec_(initial_vec.begin(), initial_vec.begin() + shape); + Mat initial_white_balance = Mat(initial_vec_, true).reshape(0, shape / 3); + ccm0 = initial_white_balance; +} + +void ColorCorrectionModel::Impl::initialLeastSquare(bool fit) +{ + Mat A, B, w; + if (weights.empty()) + { + A = src_rgbl; + B = dst_rgbl; + } + else + { + pow(weights, 0.5, w); + Mat w_; + merge(std::vector { w, w, w }, w_); + A = w_.mul(src_rgbl); + B = w_.mul(dst_rgbl); + } + solve(A.reshape(1, A.rows), B.reshape(1, B.rows), ccm0, DECOMP_SVD); + + // if fit is True, return optimalization for rgbl distance function. + if (fit) + { + ccm = ccm0; + Mat residual = A.reshape(1, A.rows) * ccm.reshape(0, shape / 3) - B.reshape(1, B.rows); + Scalar s = residual.dot(residual); + double sum = s[0]; + loss = sqrt(sum / masked_len); + } +} + +double ColorCorrectionModel::Impl::calc_loss_(Color color) +{ + Mat distlist = color.diff(*dst, distance); + Color lab = color.to(COLOR_SPACE_Lab_D50_2); + Mat dist_; + pow(distlist, 2, dist_); + if (!weights.empty()) + { + dist_ = weights.mul(dist_); + } + Scalar ss = sum(dist_); + return ss[0]; +} + +double ColorCorrectionModel::Impl::calc_loss(const Mat ccm_) +{ + Mat converted = src_rgbl.reshape(1, 0) * ccm_; + Color color(converted.reshape(3, 0), *(cs.l)); + return calc_loss_(color); +} + +void ColorCorrectionModel::Impl::fitting(void) +{ + cv::Ptr solver = cv::DownhillSolver::create(); + cv::Ptr ptr_F(new LossFunction(this)); + solver->setFunction(ptr_F); + Mat reshapeccm = ccm0.clone().reshape(0, 1); + Mat step = Mat::ones(reshapeccm.size(), CV_64F); + solver->setInitStep(step); + TermCriteria termcrit = TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, max_count, epsilon); + solver->setTermCriteria(termcrit); + double res = solver->minimize(reshapeccm); + ccm = reshapeccm.reshape(0, shape / 3); + loss = pow((res / masked_len), 0.5); +} + +Mat ColorCorrectionModel::infer(const Mat& img, bool islinear) +{ + if (!p->ccm.data) + { + CV_Error(Error::StsBadArg, "No CCM values!" ); + } + Mat img_lin = (p->linear)->linearize(img); + Mat img_ccm(img_lin.size(), img_lin.type()); + Mat ccm_ = p->ccm.reshape(0, p->shape / 3); + img_ccm = multiple(p->prepare(img_lin), ccm_); + if (islinear == true) + { + return img_ccm; + } + return p->cs.fromL(img_ccm); +} + +void ColorCorrectionModel::Impl::getColor(CONST_COLOR constcolor) +{ + dst = (GetColor::getColor(constcolor)); +} +void ColorCorrectionModel::Impl::getColor(Mat colors_, COLOR_SPACE ref_cs_) +{ + dst.reset(new Color(colors_, *GetCS::getInstance().get_cs(ref_cs_))); +} +void ColorCorrectionModel::Impl::getColor(Mat colors_, COLOR_SPACE cs_, Mat colored_) +{ + dst.reset(new Color(colors_, *GetCS::getInstance().get_cs(cs_), colored_)); +} +ColorCorrectionModel::ColorCorrectionModel(const Mat& src_, CONST_COLOR constcolor) + : p(std::make_shared()) +{ + p->src = src_; + p->getColor(constcolor); +} +ColorCorrectionModel::ColorCorrectionModel(const Mat& src_, Mat colors_, COLOR_SPACE ref_cs_) + : p(std::make_shared()) +{ + p->src = src_; + p->getColor(colors_, ref_cs_); +} +ColorCorrectionModel::ColorCorrectionModel(const Mat& src_, Mat colors_, COLOR_SPACE cs_, Mat colored_) + : p(std::make_shared()) +{ + p->src = src_; + p->getColor(colors_, cs_, colored_); +} + +void ColorCorrectionModel::setColorSpace(COLOR_SPACE cs_) +{ + p->cs = *GetCS::getInstance().get_rgb(cs_); +} +void ColorCorrectionModel::setCCM_TYPE(CCM_TYPE ccm_type_) +{ + p->ccm_type = ccm_type_; +} +void ColorCorrectionModel::setDistance(DISTANCE_TYPE distance_) +{ + p->distance = distance_; +} +void ColorCorrectionModel::setLinear(LINEAR_TYPE linear_type) +{ + p->linear_type = linear_type; +} +void ColorCorrectionModel::setLinearGamma(const double& gamma) +{ + p->gamma = gamma; +} +void ColorCorrectionModel::setLinearDegree(const int& deg) +{ + p->deg = deg; +} +void ColorCorrectionModel::setSaturatedThreshold(const double& lower, const double& upper) +{ //std::vector saturated_threshold + p->saturated_threshold = { lower, upper }; +} +void ColorCorrectionModel::setWeightsList(const Mat& weights_list) +{ + p->weights_list = weights_list; +} +void ColorCorrectionModel::setWeightCoeff(const double& weights_coeff) +{ + p->weights_coeff = weights_coeff; +} +void ColorCorrectionModel::setInitialMethod(INITIAL_METHOD_TYPE initial_method_type) +{ + p->initial_method_type = initial_method_type; +} +void ColorCorrectionModel::setMaxCount(const int& max_count_) +{ + p->max_count = max_count_; +} +void ColorCorrectionModel::setEpsilon(const double& epsilon_) +{ + p->epsilon = epsilon_; +} +void ColorCorrectionModel::run() +{ + + Mat saturate_mask = saturate(p->src, p->saturated_threshold[0], p->saturated_threshold[1]); + p->linear = getLinear(p->gamma, p->deg, p->src, *(p->dst), saturate_mask, (p->cs), p->linear_type); + p->calWeightsMasks(p->weights_list, p->weights_coeff, saturate_mask); + p->src_rgbl = p->linear->linearize(maskCopyTo(p->src, p->mask)); + p->dst->colors = maskCopyTo(p->dst->colors, p->mask); + p->dst_rgbl = p->dst->to(*(p->cs.l)).colors; + + // make no change for CCM_3x3, make change for CCM_4x3. + p->src_rgbl = p->prepare(p->src_rgbl); + + // distance function may affect the loss function and the fitting function + switch (p->distance) + { + case cv::ccm::DISTANCE_RGBL: + p->initialLeastSquare(true); + break; + default: + switch (p->initial_method_type) + { + case cv::ccm::INITIAL_METHOD_WHITE_BALANCE: + p->initialWhiteBalance(); + break; + case cv::ccm::INITIAL_METHOD_LEAST_SQUARE: + p->initialLeastSquare(); + break; + default: + CV_Error(Error::StsBadArg, "Wrong initial_methoddistance_type!" ); + break; + } + break; + } + p->fitting(); +} +Mat ColorCorrectionModel::getCCM() const +{ + return p->ccm; +} +double ColorCorrectionModel::getLoss() const +{ + return p->loss; +} +Mat ColorCorrectionModel::get_src_rgbl() const{ + return p->src_rgbl; +} +Mat ColorCorrectionModel::get_dst_rgbl() const{ + return p->dst_rgbl; +} +Mat ColorCorrectionModel::getMask() const{ + return p->mask; +} +Mat ColorCorrectionModel::getWeights() const{ + return p->weights; +} +} +} // namespace cv::ccm diff --git a/modules/mcc/src/color.cpp b/modules/mcc/src/color.cpp new file mode 100644 index 00000000000..ce00ac47e70 --- /dev/null +++ b/modules/mcc/src/color.cpp @@ -0,0 +1,398 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "color.hpp" + +namespace cv { +namespace ccm { +Color::Color() + : colors(Mat()) + , cs(*std::make_shared()) +{} +Color::Color(Mat colors_, enum COLOR_SPACE cs_) + : colors(colors_) + , cs(*GetCS::getInstance().get_cs(cs_)) +{} + +Color::Color(Mat colors_, const ColorSpace& cs_, Mat colored_) + : colors(colors_) + , cs(cs_) + , colored(colored_) +{ + grays = ~colored; +} +Color::Color(Mat colors_, enum COLOR_SPACE cs_, Mat colored_) + : colors(colors_) + , cs(*GetCS::getInstance().get_cs(cs_)) + , colored(colored_) +{ + grays = ~colored; +} + +Color::Color(Mat colors_, const ColorSpace& cs_) + : colors(colors_) + , cs(cs_) +{} + +Color Color::to(const ColorSpace& other, CAM method, bool save) +{ + if (history.count(other) == 1) + { + return *history[other]; + } + if (cs.relate(other)) + { + return Color(cs.relation(other).run(colors), other); + } + Operations ops; + ops.add(cs.to).add(XYZ(cs.io).cam(other.io, method)).add(other.from); + std::shared_ptr color(new Color(ops.run(colors), other)); + if (save) + { + history[other] = color; + } + return *color; +} +Color Color::to(COLOR_SPACE other, CAM method, bool save) +{ + return to(*GetCS::getInstance().get_cs(other), method, save); +} + +Mat Color::channel(Mat m, int i) +{ + Mat dchannels[3]; + split(m, dchannels); + return dchannels[i]; +} + +Mat Color::toGray(IO io, CAM method, bool save) +{ + XYZ xyz = *XYZ::get(io); + return channel(this->to(xyz, method, save).colors, 1); +} + +Mat Color::toLuminant(IO io, CAM method, bool save) +{ + Lab lab = *Lab::get(io); + return channel(this->to(lab, method, save).colors, 0); +} + +Mat Color::diff(Color& other, DISTANCE_TYPE method) +{ + return diff(other, cs.io, method); +} + +Mat Color::diff(Color& other, IO io, DISTANCE_TYPE method) +{ + Lab lab = *Lab::get(io); + switch (method) + { + case cv::ccm::DISTANCE_CIE76: + case cv::ccm::DISTANCE_CIE94_GRAPHIC_ARTS: + case cv::ccm::DISTANCE_CIE94_TEXTILES: + case cv::ccm::DISTANCE_CIE2000: + case cv::ccm::DISTANCE_CMC_1TO1: + case cv::ccm::DISTANCE_CMC_2TO1: + return distance(to(lab).colors, other.to(lab).colors, method); + case cv::ccm::DISTANCE_RGB: + return distance(to(*cs.nl).colors, other.to(*cs.nl).colors, method); + case cv::ccm::DISTANCE_RGBL: + return distance(to(*cs.l).colors, other.to(*cs.l).colors, method); + default: + CV_Error(Error::StsBadArg, "Wrong method!" ); + break; + } +} + +void Color::getGray(double JDN) +{ + if (!grays.empty()) + { + return; + } + Mat lab = to(COLOR_SPACE_Lab_D65_2).colors; + Mat gray(colors.size(), colors.type()); + int fromto[] = { 0, 0, -1, 1, -1, 2 }; + mixChannels(&lab, 1, &gray, 1, fromto, 3); + Mat d = distance(lab, gray, DISTANCE_CIE2000); + this->grays = d < JDN; + this->colored = ~grays; +} + +Color Color::operator[](Mat mask) +{ + return Color(maskCopyTo(colors, mask), cs); +} + +Mat GetColor::getColorChecker(const double* checker, int row) +{ + Mat res(row, 1, CV_64FC3); + for (int i = 0; i < row; ++i) + { + res.at(i, 0) = Vec3d(checker[3 * i], checker[3 * i + 1], checker[3 * i + 2]); + } + return res; +} + +Mat GetColor::getColorCheckerMASK(const uchar* checker, int row) +{ + Mat res(row, 1, CV_8U); + for (int i = 0; i < row; ++i) + { + res.at(i, 0) = checker[i]; + } + return res; +} + +std::shared_ptr GetColor::getColor(CONST_COLOR const_color) +{ + + /** @brief Data is from https://www.imatest.com/wp-content/uploads/2011/11/Lab-data-Iluminate-D65-D50-spectro.xls + see Miscellaneous.md for details. +*/ + static const double ColorChecker2005_LAB_D50_2[24][3] = { { 37.986, 13.555, 14.059 }, + { 65.711, 18.13, 17.81 }, + { 49.927, -4.88, -21.925 }, + { 43.139, -13.095, 21.905 }, + { 55.112, 8.844, -25.399 }, + { 70.719, -33.397, -0.199 }, + { 62.661, 36.067, 57.096 }, + { 40.02, 10.41, -45.964 }, + { 51.124, 48.239, 16.248 }, + { 30.325, 22.976, -21.587 }, + { 72.532, -23.709, 57.255 }, + { 71.941, 19.363, 67.857 }, + { 28.778, 14.179, -50.297 }, + { 55.261, -38.342, 31.37 }, + { 42.101, 53.378, 28.19 }, + { 81.733, 4.039, 79.819 }, + { 51.935, 49.986, -14.574 }, + { 51.038, -28.631, -28.638 }, + { 96.539, -0.425, 1.186 }, + { 81.257, -0.638, -0.335 }, + { 66.766, -0.734, -0.504 }, + { 50.867, -0.153, -0.27 }, + { 35.656, -0.421, -1.231 }, + { 20.461, -0.079, -0.973 } }; + + static const uchar ColorChecker2005_COLORED_MASK[24] = { 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0 }; + static const double Vinyl_LAB_D50_2[18][3] = { { 100, 0.00520000001, -0.0104 }, + { 73.0833969, -0.819999993, -2.02099991 }, + { 62.493, 0.425999999, -2.23099995 }, + { 50.4640007, 0.446999997, -2.32399988 }, + { 37.7970009, 0.0359999985, -1.29700005 }, + { 0, 0, 0 }, + { 51.5880013, 73.5179977, 51.5690002 }, + { 93.6989975, -15.7340002, 91.9420013 }, + { 69.4079971, -46.5940018, 50.4869995 }, + { 66.61000060000001, -13.6789999, -43.1720009 }, + { 11.7110004, 16.9799995, -37.1759987 }, + { 51.973999, 81.9440002, -8.40699959 }, + { 40.5489998, 50.4399986, 24.8490009 }, + { 60.8160019, 26.0690002, 49.4420013 }, + { 52.2529984, -19.9500008, -23.9960003 }, + { 51.2859993, 48.4700012, -15.0579996 }, + { 68.70700069999999, 12.2959995, 16.2129993 }, + { 63.6839981, 10.2930002, 16.7639999 } }; + static const uchar Vinyl_COLORED_MASK[18] = { 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1 }; + static const double DigitalSG_LAB_D50_2[140][3] = { { 96.55, -0.91, 0.57 }, + { 6.43, -0.06, -0.41 }, + { 49.7, -0.18, 0.03 }, + { 96.5, -0.89, 0.59 }, + { 6.5, -0.06, -0.44 }, + { 49.66, -0.2, 0.01 }, + { 96.52, -0.91, 0.58 }, + { 6.49, -0.02, -0.28 }, + { 49.72, -0.2, 0.04 }, + { 96.43, -0.91, 0.67 }, + { 49.72, -0.19, 0 }, + { 32.6, 51.58, -10.85 }, + { 60.75, 26.22, -18.6 }, + { 28.69, 48.28, -39 }, + { 49.38, -15.43, -48.48 }, + { 60.63, -30.77, -26.23 }, + { 19.29, -26.37, -6.15 }, + { 60.15, -41.77, -12.6 }, + { 21.42, 1.67, 8.79 }, + { 49.69, -0.2, 0.01 }, + { 6.5, -0.03, -0.67 }, + { 21.82, 17.33, -18.35 }, + { 41.53, 18.48, -37.26 }, + { 19.99, -0.16, -36.29 }, + { 60.16, -18.45, -31.42 }, + { 19.94, -17.92, -20.96 }, + { 60.68, -6.05, -32.81 }, + { 50.81, -49.8, -9.63 }, + { 60.65, -39.77, 20.76 }, + { 6.53, -0.03, -0.43 }, + { 96.56, -0.91, 0.59 }, + { 84.19, -1.95, -8.23 }, + { 84.75, 14.55, 0.23 }, + { 84.87, -19.07, -0.82 }, + { 85.15, 13.48, 6.82 }, + { 84.17, -10.45, 26.78 }, + { 61.74, 31.06, 36.42 }, + { 64.37, 20.82, 18.92 }, + { 50.4, -53.22, 14.62 }, + { 96.51, -0.89, 0.65 }, + { 49.74, -0.19, 0.03 }, + { 31.91, 18.62, 21.99 }, + { 60.74, 38.66, 70.97 }, + { 19.35, 22.23, -58.86 }, + { 96.52, -0.91, 0.62 }, + { 6.66, 0, -0.3 }, + { 76.51, 20.81, 22.72 }, + { 72.79, 29.15, 24.18 }, + { 22.33, -20.7, 5.75 }, + { 49.7, -0.19, 0.01 }, + { 6.53, -0.05, -0.61 }, + { 63.42, 20.19, 19.22 }, + { 34.94, 11.64, -50.7 }, + { 52.03, -44.15, 39.04 }, + { 79.43, 0.29, -0.17 }, + { 30.67, -0.14, -0.53 }, + { 63.6, 14.44, 26.07 }, + { 64.37, 14.5, 17.05 }, + { 60.01, -44.33, 8.49 }, + { 6.63, -0.01, -0.47 }, + { 96.56, -0.93, 0.59 }, + { 46.37, -5.09, -24.46 }, + { 47.08, 52.97, 20.49 }, + { 36.04, 64.92, 38.51 }, + { 65.05, 0, -0.32 }, + { 40.14, -0.19, -0.38 }, + { 43.77, 16.46, 27.12 }, + { 64.39, 17, 16.59 }, + { 60.79, -29.74, 41.5 }, + { 96.48, -0.89, 0.64 }, + { 49.75, -0.21, 0.01 }, + { 38.18, -16.99, 30.87 }, + { 21.31, 29.14, -27.51 }, + { 80.57, 3.85, 89.61 }, + { 49.71, -0.2, 0.03 }, + { 60.27, 0.08, -0.41 }, + { 67.34, 14.45, 16.9 }, + { 64.69, 16.95, 18.57 }, + { 51.12, -49.31, 44.41 }, + { 49.7, -0.2, 0.02 }, + { 6.67, -0.05, -0.64 }, + { 51.56, 9.16, -26.88 }, + { 70.83, -24.26, 64.77 }, + { 48.06, 55.33, -15.61 }, + { 35.26, -0.09, -0.24 }, + { 75.16, 0.25, -0.2 }, + { 44.54, 26.27, 38.93 }, + { 35.91, 16.59, 26.46 }, + { 61.49, -52.73, 47.3 }, + { 6.59, -0.05, -0.5 }, + { 96.58, -0.9, 0.61 }, + { 68.93, -34.58, -0.34 }, + { 69.65, 20.09, 78.57 }, + { 47.79, -33.18, -30.21 }, + { 15.94, -0.42, -1.2 }, + { 89.02, -0.36, -0.48 }, + { 63.43, 25.44, 26.25 }, + { 65.75, 22.06, 27.82 }, + { 61.47, 17.1, 50.72 }, + { 96.53, -0.89, 0.66 }, + { 49.79, -0.2, 0.03 }, + { 85.17, 10.89, 17.26 }, + { 89.74, -16.52, 6.19 }, + { 84.55, 5.07, -6.12 }, + { 84.02, -13.87, -8.72 }, + { 70.76, 0.07, -0.35 }, + { 45.59, -0.05, 0.23 }, + { 20.3, 0.07, -0.32 }, + { 61.79, -13.41, 55.42 }, + { 49.72, -0.19, 0.02 }, + { 6.77, -0.05, -0.44 }, + { 21.85, 34.37, 7.83 }, + { 42.66, 67.43, 48.42 }, + { 60.33, 36.56, 3.56 }, + { 61.22, 36.61, 17.32 }, + { 62.07, 52.8, 77.14 }, + { 72.42, -9.82, 89.66 }, + { 62.03, 3.53, 57.01 }, + { 71.95, -27.34, 73.69 }, + { 6.59, -0.04, -0.45 }, + { 49.77, -0.19, 0.04 }, + { 41.84, 62.05, 10.01 }, + { 19.78, 29.16, -7.85 }, + { 39.56, 65.98, 33.71 }, + { 52.39, 68.33, 47.84 }, + { 81.23, 24.12, 87.51 }, + { 81.8, 6.78, 95.75 }, + { 71.72, -16.23, 76.28 }, + { 20.31, 14.45, 16.74 }, + { 49.68, -0.19, 0.05 }, + { 96.48, -0.88, 0.68 }, + { 49.69, -0.18, 0.03 }, + { 6.39, -0.04, -0.33 }, + { 96.54, -0.9, 0.67 }, + { 49.72, -0.18, 0.05 }, + { 6.49, -0.03, -0.41 }, + { 96.51, -0.9, 0.69 }, + { 49.7, -0.19, 0.07 }, + { 6.47, 0, -0.38 }, + { 96.46, -0.89, 0.7 } }; + + switch (const_color) + { + + case cv::ccm::COLORCHECKER_Macbeth: + { + Mat ColorChecker2005_LAB_D50_2_ = GetColor::getColorChecker(*ColorChecker2005_LAB_D50_2, 24); + Mat ColorChecker2005_COLORED_MASK_ = GetColor::getColorCheckerMASK(ColorChecker2005_COLORED_MASK, 24); + std::shared_ptr Macbeth_D50_2 = std::make_shared(ColorChecker2005_LAB_D50_2_, COLOR_SPACE_Lab_D50_2, ColorChecker2005_COLORED_MASK_); + return Macbeth_D50_2; + } + + case cv::ccm::COLORCHECKER_Vinyl: + { + Mat Vinyl_LAB_D50_2__ = GetColor::getColorChecker(*Vinyl_LAB_D50_2, 18); + Mat Vinyl_COLORED_MASK__ = GetColor::getColorCheckerMASK(Vinyl_COLORED_MASK, 18); + std::shared_ptr Vinyl_D50_2 = std::make_shared(Vinyl_LAB_D50_2__, COLOR_SPACE_Lab_D50_2, Vinyl_COLORED_MASK__); + return Vinyl_D50_2; + } + + case cv::ccm::COLORCHECKER_DigitalSG: + { + Mat DigitalSG_LAB_D50_2__ = GetColor::getColorChecker(*DigitalSG_LAB_D50_2, 140); + std::shared_ptr DigitalSG_D50_2 = std::make_shared(DigitalSG_LAB_D50_2__, COLOR_SPACE_Lab_D50_2); + return DigitalSG_D50_2; + } + } + CV_Error(Error::StsNotImplemented, ""); +} + +} +} // namespace cv::ccm diff --git a/modules/mcc/src/color.hpp b/modules/mcc/src/color.hpp new file mode 100644 index 00000000000..57ead3558c7 --- /dev/null +++ b/modules/mcc/src/color.hpp @@ -0,0 +1,118 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_COLOR_HPP__ +#define __OPENCV_MCC_COLOR_HPP__ + +#include "distance.hpp" +#include "colorspace.hpp" +#include "opencv2/mcc/ccm.hpp" + +namespace cv { +namespace ccm { + +/** @brief Color defined by color_values and color space +*/ + +class Color +{ +public: + /** @param grays mask of grayscale color + @param colored mask of colored color + @param history storage of historical conversion + */ + Mat colors; + const ColorSpace& cs; + Mat grays; + Mat colored; + std::map> history; + Color(); + Color(Mat colors_, enum COLOR_SPACE cs); + Color(Mat colors_, enum COLOR_SPACE cs, Mat colored); + Color(Mat colors_, const ColorSpace& cs, Mat colored); + Color(Mat colors_, const ColorSpace& cs); + virtual ~Color() {}; + + /** @brief Change to other color space. + The conversion process incorporates linear transformations to speed up. + @param other type of ColorSpace. + @param method the chromatic adapation method. + @param save when save if True, get data from history first. + @return Color. + */ + Color to(COLOR_SPACE other, CAM method = BRADFORD, bool save = true); + Color to(const ColorSpace& other, CAM method = BRADFORD, bool save = true); + + /** @brief Channels split. + @return each channel. + */ + Mat channel(Mat m, int i); + + /** @brief To Gray. + */ + Mat toGray(IO io, CAM method = BRADFORD, bool save = true); + + /** @brief To Luminant. + */ + Mat toLuminant(IO io, CAM method = BRADFORD, bool save = true); + + /** @brief Diff without IO. + @param other type of Color. + @param method type of distance. + @return distance between self and other + */ + Mat diff(Color& other, DISTANCE_TYPE method = DISTANCE_CIE2000); + + /** @brief Diff with IO. + @param other type of Color. + @param io type of IO. + @param method type of distance. + @return distance between self and other + */ + Mat diff(Color& other, IO io, DISTANCE_TYPE method = DISTANCE_CIE2000); + + /** @brief Calculate gray mask. + */ + void getGray(double JDN = 2.0); + + /** @brief Operator for mask copy. + */ + Color operator[](Mat mask); +}; + +class GetColor +{ +public: + static std::shared_ptr getColor(CONST_COLOR const_color); + static Mat getColorChecker(const double* checker, int row); + static Mat getColorCheckerMASK(const uchar* checker, int row); +}; + +} +} // namespace cv::ccm + +#endif \ No newline at end of file diff --git a/modules/mcc/src/colorspace.cpp b/modules/mcc/src/colorspace.cpp new file mode 100644 index 00000000000..9dfe3f6e125 --- /dev/null +++ b/modules/mcc/src/colorspace.cpp @@ -0,0 +1,789 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "precomp.hpp" + +#include "colorspace.hpp" +#include "operations.hpp" +#include "io.hpp" + +namespace cv { +namespace ccm { +static const std::vector& getIlluminants(const IO& io) +{ + static const std::map> illuminants = { + { IO::getIOs(A_2), { 1.098466069456375, 1, 0.3558228003436005 } }, + { IO::getIOs(A_10), { 1.111420406956693, 1, 0.3519978321919493 } }, + { IO::getIOs(D50_2), { 0.9642119944211994, 1, 0.8251882845188288 } }, + { IO::getIOs(D50_10), { 0.9672062750333777, 1, 0.8142801513128616 } }, + { IO::getIOs(D55_2), { 0.956797052643698, 1, 0.9214805860173273 } }, + { IO::getIOs(D55_10), { 0.9579665682254781, 1, 0.9092525159847462 } }, + { IO::getIOs(D65_2), { 0.95047, 1., 1.08883 } }, + { IO::getIOs(D65_10), { 0.94811, 1., 1.07304 } }, + { IO::getIOs(D75_2), { 0.9497220898840717, 1, 1.226393520724154 } }, + { IO::getIOs(D75_10), { 0.9441713925645873, 1, 1.2064272211720228 } }, + { IO::getIOs(E_2), { 1., 1., 1. } }, + { IO::getIOs(E_10), { 1., 1., 1. } }, + }; + auto it = illuminants.find(io); + CV_Assert(it != illuminants.end()); + return it->second; +}; + +/* @brief Basic class for ColorSpace. + */ +bool ColorSpace::relate(const ColorSpace& other) const +{ + return (type == other.type) && (io == other.io); +}; + +Operations ColorSpace::relation(const ColorSpace& /*other*/) const +{ + return Operations::get_IDENTITY_OPS(); +} + +bool ColorSpace::operator<(const ColorSpace& other) const +{ + return (io < other.io || (io == other.io && type < other.type) || (io == other.io && type == other.type && linear < other.linear)); +} + +/* @brief Base of RGB color space; + * the argument values are from AdobeRGB; + * Data from https://en.wikipedia.org/wiki/Adobe_RGB_color_space + */ +Operations RGBBase_::relation(const ColorSpace& other) const +{ + if (linear == other.linear) + { + return Operations::get_IDENTITY_OPS(); + } + if (linear) + { + return Operations({ Operation(fromL) }); + } + return Operations({ Operation(toL) }); +} + +/* @brief Initial operations. + */ +void RGBBase_::init() +{ + setParameter(); + calLinear(); + calM(); + calOperations(); +} + +/* @brief Produce color space instance with linear and non-linear versions. + * @param rgbl type of RGBBase_. + */ +void RGBBase_::bind(RGBBase_& rgbl) +{ + init(); + rgbl.init(); + l = &rgbl; + rgbl.l = &rgbl; + nl = this; + rgbl.nl = this; +} + +/* @brief Calculation of M_RGBL2XYZ_base. + */ +void RGBBase_::calM() +{ + Mat XYZr, XYZg, XYZb, XYZ_rgbl, Srgb; + XYZr = Mat(xyY2XYZ({ xr, yr }), true); + XYZg = Mat(xyY2XYZ({ xg, yg }), true); + XYZb = Mat(xyY2XYZ({ xb, yb }), true); + merge(std::vector { XYZr, XYZg, XYZb }, XYZ_rgbl); + XYZ_rgbl = XYZ_rgbl.reshape(1, XYZ_rgbl.rows); + Mat XYZw = Mat(getIlluminants(io), true); + solve(XYZ_rgbl, XYZw, Srgb); + merge(std::vector { Srgb.at(0) * XYZr, Srgb.at(1) * XYZg, + Srgb.at(2) * XYZb }, + M_to); + M_to = M_to.reshape(1, M_to.rows); + M_from = M_to.inv(); +}; + +/* @brief operations to or from XYZ. + */ +void RGBBase_::calOperations() +{ + // rgb -> rgbl + toL = [this](Mat rgb) -> Mat { return toLFunc(rgb); }; + + // rgbl -> rgb + fromL = [this](Mat rgbl) -> Mat { return fromLFunc(rgbl); }; + + if (linear) + { + to = Operations({ Operation(M_to.t()) }); + from = Operations({ Operation(M_from.t()) }); + } + else + { + to = Operations({ Operation(toL), Operation(M_to.t()) }); + from = Operations({ Operation(M_from.t()), Operation(fromL) }); + } +} + +Mat RGBBase_::toLFunc(Mat& /*rgb*/) { return Mat(); } + +Mat RGBBase_::fromLFunc(Mat& /*rgbl*/) { return Mat(); } + +/* @brief Base of Adobe RGB color space; + */ + +Mat AdobeRGBBase_::toLFunc(Mat& rgb) { return gammaCorrection(rgb, gamma); } + +Mat AdobeRGBBase_::fromLFunc(Mat& rgbl) +{ + return gammaCorrection(rgbl, 1. / gamma); +} + +/* @brief Base of sRGB color space; + */ + +void sRGBBase_::calLinear() +{ + alpha = a + 1; + K0 = a / (gamma - 1); + phi = (pow(alpha, gamma) * pow(gamma - 1, gamma - 1)) / (pow(a, gamma - 1) * pow(gamma, gamma)); + beta = K0 / phi; +} + +/* @brief Used by toLFunc. + */ +double sRGBBase_::toLFuncEW(double& x) +{ + if (x > K0) + { + return pow(((x + alpha - 1) / alpha), gamma); + } + else if (x >= -K0) + { + return x / phi; + } + else + { + return -(pow(((-x + alpha - 1) / alpha), gamma)); + } +} + +/* @brief Linearization. + * @param rgb the input array, type of cv::Mat. + * @return the output array, type of cv::Mat. + */ +Mat sRGBBase_::toLFunc(Mat& rgb) +{ + return elementWise(rgb, + [this](double a_) -> double { return toLFuncEW(a_); }); +} + +/* @brief Used by fromLFunc. + */ +double sRGBBase_::fromLFuncEW(double& x) +{ + if (x > beta) + { + return alpha * pow(x, 1 / gamma) - (alpha - 1); + } + else if (x >= -beta) + { + return x * phi; + } + else + { + return -(alpha * pow(-x, 1 / gamma) - (alpha - 1)); + } +} + +/* @brief Delinearization. + * @param rgbl the input array, type of cv::Mat. + * @return the output array, type of cv::Mat. + */ +Mat sRGBBase_::fromLFunc(Mat& rgbl) +{ + return elementWise(rgbl, + [this](double a_) -> double { return fromLFuncEW(a_); }); +} + +/* @brief sRGB color space. + * data from https://en.wikipedia.org/wiki/SRGB. + */ +void sRGB_::setParameter() +{ + xr = 0.64; + yr = 0.33; + xg = 0.3; + yg = 0.6; + xb = 0.15; + yb = 0.06; + a = 0.055; + gamma = 2.4; +} + +/* @brief Adobe RGB color space. + */ +void AdobeRGB_::setParameter() +{ + xr = 0.64; + yr = 0.33; + xg = 0.21; + yg = 0.71; + xb = 0.15; + yb = 0.06; + gamma = 2.2; +} + +/* @brief Wide-gamut RGB color space. + * data from https://en.wikipedia.org/wiki/Wide-gamut_RGB_color_space. + */ +void WideGamutRGB_::setParameter() +{ + xr = 0.7347; + yr = 0.2653; + xg = 0.1152; + yg = 0.8264; + xb = 0.1566; + yb = 0.0177; + gamma = 2.2; +} + +/* @brief ProPhoto RGB color space. + * data from https://en.wikipedia.org/wiki/ProPhoto_RGB_color_space. + */ +void ProPhotoRGB_::setParameter() +{ + xr = 0.734699; + yr = 0.265301; + xg = 0.159597; + yg = 0.840403; + xb = 0.036598; + yb = 0.000105; + gamma = 1.8; +} + +/* @brief DCI-P3 RGB color space. + * data from https://en.wikipedia.org/wiki/DCI-P3. + */ + +void DCI_P3_RGB_::setParameter() +{ + xr = 0.68; + yr = 0.32; + xg = 0.265; + yg = 0.69; + xb = 0.15; + yb = 0.06; + gamma = 2.2; +} + +/* @brief Apple RGB color space. + * data from + * http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html. + */ +void AppleRGB_::setParameter() +{ + xr = 0.625; + yr = 0.34; + xg = 0.28; + yg = 0.595; + xb = 0.155; + yb = 0.07; + gamma = 1.8; +} + +/* @brief REC_709 RGB color space. + * data from https://en.wikipedia.org/wiki/Rec._709. + */ +void REC_709_RGB_::setParameter() +{ + xr = 0.64; + yr = 0.33; + xg = 0.3; + yg = 0.6; + xb = 0.15; + yb = 0.06; + a = 0.099; + gamma = 1 / 0.45; +} + +/* @brief REC_2020 RGB color space. + * data from https://en.wikipedia.org/wiki/Rec._2020. + */ + +void REC_2020_RGB_::setParameter() +{ + xr = 0.708; + yr = 0.292; + xg = 0.17; + yg = 0.797; + xb = 0.131; + yb = 0.046; + a = 0.09929682680944; + gamma = 1 / 0.45; +} + +Operations XYZ::cam(IO dio, CAM method) +{ + return (io == dio) ? Operations() + : Operations({ Operation(cam_(io, dio, method).t()) }); +} +Mat XYZ::cam_(IO sio, IO dio, CAM method) const +{ + static std::map, Mat> cams; + + if (sio == dio) + { + return Mat::eye(cv::Size(3, 3), CV_64FC1); + } + if (cams.count(std::make_tuple(dio, sio, method)) == 1) + { + return cams[std::make_tuple(dio, sio, method)]; + } + /* @brief XYZ color space. + * Chromatic adaption matrices. + */ + + static const Mat Von_Kries = (Mat_(3, 3) << 0.40024, 0.7076, -0.08081, -0.2263, 1.16532, 0.0457, 0., 0., 0.91822); + static const Mat Bradford = (Mat_(3, 3) << 0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296); + static const std::map> MAs = { + { IDENTITY, { Mat::eye(Size(3, 3), CV_64FC1), Mat::eye(Size(3, 3), CV_64FC1) } }, + { VON_KRIES, { Von_Kries, Von_Kries.inv() } }, + { BRADFORD, { Bradford, Bradford.inv() } } + }; + + // Function from http://www.brucelindbloom.com/index.html?ColorCheckerRGB.html. + Mat XYZws = Mat(getIlluminants(dio)); + Mat XYZWd = Mat(getIlluminants(sio)); + Mat MA = MAs.at(method)[0]; + Mat MA_inv = MAs.at(method)[1]; + Mat M = MA_inv * Mat::diag((MA * XYZws) / (MA * XYZWd)) * MA; + cams[std::make_tuple(dio, sio, method)] = M; + cams[std::make_tuple(sio, dio, method)] = M.inv(); + return M; +} + +std::shared_ptr XYZ::get(IO io) +{ + static std::map> xyz_cs; + + if (xyz_cs.count(io) == 1) + { + return xyz_cs[io]; + } + std::shared_ptr XYZ_CS = std::make_shared(io); + xyz_cs[io] = XYZ_CS; + return xyz_cs[io]; +} + +/* @brief Lab color space. + */ +Lab::Lab(IO io_) + : ColorSpace(io_, "Lab", true) +{ + to = { Operation([this](Mat src) -> Mat { return tosrc(src); }) }; + from = { Operation([this](Mat src) -> Mat { return fromsrc(src); }) }; +} + +Vec3d Lab::fromxyz(cv::Vec3d& xyz) +{ + auto& il = getIlluminants(io); + double x = xyz[0] / il[0], + y = xyz[1] / il[1], + z = xyz[2] / il[2]; + auto f = [](double t) -> double { + return t > t0 ? std::cbrt(t) : (m * t + c); + }; + double fx = f(x), fy = f(y), fz = f(z); + return { 116. * fy - 16., 500 * (fx - fy), 200 * (fy - fz) }; +} + +/* @brief Calculate From. + * @param src the input array, type of cv::Mat. + * @return the output array, type of cv::Mat + */ +Mat Lab::fromsrc(Mat& src) +{ + return channelWise(src, + [this](cv::Vec3d a) -> cv::Vec3d { return fromxyz(a); }); +} + +Vec3d Lab::tolab(cv::Vec3d& lab) +{ + auto f_inv = [](double t) -> double { + return t > delta ? pow(t, 3.0) : (t - c) / m; + }; + double L = (lab[0] + 16.) / 116., a = lab[1] / 500., b = lab[2] / 200.; + auto& il = getIlluminants(io); + return { il[0] * f_inv(L + a), + il[1] * f_inv(L), + il[2] * f_inv(L - b) }; +} + +/* @brief Calculate To. + * @param src the input array, type of cv::Mat. + * @return the output array, type of cv::Mat + */ +Mat Lab::tosrc(Mat& src) +{ + return channelWise(src, + [this](cv::Vec3d a) -> cv::Vec3d { return tolab(a); }); +} + +std::shared_ptr Lab::get(IO io) +{ + static std::map> lab_cs; + + if (lab_cs.count(io) == 1) + { + return lab_cs[io]; + } + std::shared_ptr Lab_CS(new Lab(io)); + lab_cs[io] = Lab_CS; + return lab_cs[io]; +} + +GetCS::GetCS() +{ + // nothing +} + +GetCS& GetCS::getInstance() +{ + static GetCS instance; + return instance; +} + +std::shared_ptr GetCS::get_rgb(enum COLOR_SPACE cs_name) +{ + switch (cs_name) + { + case cv::ccm::COLOR_SPACE_sRGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr sRGB_CS(new sRGB_(false)); + std::shared_ptr sRGBL_CS(new sRGB_(true)); + (*sRGB_CS).bind(*sRGBL_CS); + map_cs[COLOR_SPACE_sRGB] = sRGB_CS; + map_cs[COLOR_SPACE_sRGBL] = sRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_AdobeRGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr AdobeRGB_CS(new AdobeRGB_(false)); + std::shared_ptr AdobeRGBL_CS(new AdobeRGB_(true)); + (*AdobeRGB_CS).bind(*AdobeRGBL_CS); + map_cs[COLOR_SPACE_AdobeRGB] = AdobeRGB_CS; + map_cs[COLOR_SPACE_AdobeRGBL] = AdobeRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_WideGamutRGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr WideGamutRGB_CS(new WideGamutRGB_(false)); + std::shared_ptr WideGamutRGBL_CS(new WideGamutRGB_(true)); + (*WideGamutRGB_CS).bind(*WideGamutRGBL_CS); + map_cs[COLOR_SPACE_WideGamutRGB] = WideGamutRGB_CS; + map_cs[COLOR_SPACE_WideGamutRGBL] = WideGamutRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_ProPhotoRGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr ProPhotoRGB_CS(new ProPhotoRGB_(false)); + std::shared_ptr ProPhotoRGBL_CS(new ProPhotoRGB_(true)); + (*ProPhotoRGB_CS).bind(*ProPhotoRGBL_CS); + map_cs[COLOR_SPACE_ProPhotoRGB] = ProPhotoRGB_CS; + map_cs[COLOR_SPACE_ProPhotoRGBL] = ProPhotoRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_DCI_P3_RGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr DCI_P3_RGB_CS(new DCI_P3_RGB_(false)); + std::shared_ptr DCI_P3_RGBL_CS(new DCI_P3_RGB_(true)); + (*DCI_P3_RGB_CS).bind(*DCI_P3_RGBL_CS); + map_cs[COLOR_SPACE_DCI_P3_RGB] = DCI_P3_RGB_CS; + map_cs[COLOR_SPACE_DCI_P3_RGBL] = DCI_P3_RGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_AppleRGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr AppleRGB_CS(new AppleRGB_(false)); + std::shared_ptr AppleRGBL_CS(new AppleRGB_(true)); + (*AppleRGB_CS).bind(*AppleRGBL_CS); + map_cs[COLOR_SPACE_AppleRGB] = AppleRGB_CS; + map_cs[COLOR_SPACE_AppleRGBL] = AppleRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_REC_709_RGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr REC_709_RGB_CS(new REC_709_RGB_(false)); + std::shared_ptr REC_709_RGBL_CS(new REC_709_RGB_(true)); + (*REC_709_RGB_CS).bind(*REC_709_RGBL_CS); + map_cs[COLOR_SPACE_REC_709_RGB] = REC_709_RGB_CS; + map_cs[COLOR_SPACE_REC_709_RGBL] = REC_709_RGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_REC_2020_RGB: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr REC_2020_RGB_CS(new REC_2020_RGB_(false)); + std::shared_ptr REC_2020_RGBL_CS(new REC_2020_RGB_(true)); + (*REC_2020_RGB_CS).bind(*REC_2020_RGBL_CS); + map_cs[COLOR_SPACE_REC_2020_RGB] = REC_2020_RGB_CS; + map_cs[COLOR_SPACE_REC_2020_RGBL] = REC_2020_RGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_sRGBL: + case cv::ccm::COLOR_SPACE_AdobeRGBL: + case cv::ccm::COLOR_SPACE_WideGamutRGBL: + case cv::ccm::COLOR_SPACE_ProPhotoRGBL: + case cv::ccm::COLOR_SPACE_DCI_P3_RGBL: + case cv::ccm::COLOR_SPACE_AppleRGBL: + case cv::ccm::COLOR_SPACE_REC_709_RGBL: + case cv::ccm::COLOR_SPACE_REC_2020_RGBL: + CV_Error(Error::StsBadArg, "linear RGB colorspaces are not supported, you should assigned as normal RGB color space"); + break; + + default: + CV_Error(Error::StsBadArg, "Only RGB color spaces are supported"); + } + return (std::dynamic_pointer_cast)(map_cs[cs_name]); +} + +std::shared_ptr GetCS::get_cs(enum COLOR_SPACE cs_name) +{ + switch (cs_name) + { + case cv::ccm::COLOR_SPACE_sRGB: + case cv::ccm::COLOR_SPACE_sRGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr sRGB_CS(new sRGB_(false)); + std::shared_ptr sRGBL_CS(new sRGB_(true)); + (*sRGB_CS).bind(*sRGBL_CS); + map_cs[COLOR_SPACE_sRGB] = sRGB_CS; + map_cs[COLOR_SPACE_sRGBL] = sRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_AdobeRGB: + case cv::ccm::COLOR_SPACE_AdobeRGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr AdobeRGB_CS(new AdobeRGB_(false)); + std::shared_ptr AdobeRGBL_CS(new AdobeRGB_(true)); + (*AdobeRGB_CS).bind(*AdobeRGBL_CS); + map_cs[COLOR_SPACE_AdobeRGB] = AdobeRGB_CS; + map_cs[COLOR_SPACE_AdobeRGBL] = AdobeRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_WideGamutRGB: + case cv::ccm::COLOR_SPACE_WideGamutRGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr WideGamutRGB_CS(new WideGamutRGB_(false)); + std::shared_ptr WideGamutRGBL_CS(new WideGamutRGB_(true)); + (*WideGamutRGB_CS).bind(*WideGamutRGBL_CS); + map_cs[COLOR_SPACE_WideGamutRGB] = WideGamutRGB_CS; + map_cs[COLOR_SPACE_WideGamutRGBL] = WideGamutRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_ProPhotoRGB: + case cv::ccm::COLOR_SPACE_ProPhotoRGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr ProPhotoRGB_CS(new ProPhotoRGB_(false)); + std::shared_ptr ProPhotoRGBL_CS(new ProPhotoRGB_(true)); + (*ProPhotoRGB_CS).bind(*ProPhotoRGBL_CS); + map_cs[COLOR_SPACE_ProPhotoRGB] = ProPhotoRGB_CS; + map_cs[COLOR_SPACE_ProPhotoRGBL] = ProPhotoRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_DCI_P3_RGB: + case cv::ccm::COLOR_SPACE_DCI_P3_RGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr DCI_P3_RGB_CS(new DCI_P3_RGB_(false)); + std::shared_ptr DCI_P3_RGBL_CS(new DCI_P3_RGB_(true)); + (*DCI_P3_RGB_CS).bind(*DCI_P3_RGBL_CS); + map_cs[COLOR_SPACE_DCI_P3_RGB] = DCI_P3_RGB_CS; + map_cs[COLOR_SPACE_DCI_P3_RGBL] = DCI_P3_RGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_AppleRGB: + case cv::ccm::COLOR_SPACE_AppleRGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr AppleRGB_CS(new AppleRGB_(false)); + std::shared_ptr AppleRGBL_CS(new AppleRGB_(true)); + (*AppleRGB_CS).bind(*AppleRGBL_CS); + map_cs[COLOR_SPACE_AppleRGB] = AppleRGB_CS; + map_cs[COLOR_SPACE_AppleRGBL] = AppleRGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_REC_709_RGB: + case cv::ccm::COLOR_SPACE_REC_709_RGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr REC_709_RGB_CS(new REC_709_RGB_(false)); + std::shared_ptr REC_709_RGBL_CS(new REC_709_RGB_(true)); + (*REC_709_RGB_CS).bind(*REC_709_RGBL_CS); + map_cs[COLOR_SPACE_REC_709_RGB] = REC_709_RGB_CS; + map_cs[COLOR_SPACE_REC_709_RGBL] = REC_709_RGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_REC_2020_RGB: + case cv::ccm::COLOR_SPACE_REC_2020_RGBL: + { + if (map_cs.count(cs_name) < 1) + { + std::shared_ptr REC_2020_RGB_CS(new REC_2020_RGB_(false)); + std::shared_ptr REC_2020_RGBL_CS(new REC_2020_RGB_(true)); + (*REC_2020_RGB_CS).bind(*REC_2020_RGBL_CS); + map_cs[COLOR_SPACE_REC_2020_RGB] = REC_2020_RGB_CS; + map_cs[COLOR_SPACE_REC_2020_RGBL] = REC_2020_RGBL_CS; + } + break; + } + case cv::ccm::COLOR_SPACE_XYZ_D65_2: + return XYZ::get(IO::getIOs(D65_2)); + break; + case cv::ccm::COLOR_SPACE_XYZ_D50_2: + return XYZ::get(IO::getIOs(D50_2)); + break; + case cv::ccm::COLOR_SPACE_XYZ_D65_10: + return XYZ::get(IO::getIOs(D65_10)); + break; + case cv::ccm::COLOR_SPACE_XYZ_D50_10: + return XYZ::get(IO::getIOs(D50_10)); + break; + case cv::ccm::COLOR_SPACE_XYZ_A_2: + return XYZ::get(IO::getIOs(A_2)); + break; + case cv::ccm::COLOR_SPACE_XYZ_A_10: + return XYZ::get(IO::getIOs(A_10)); + break; + case cv::ccm::COLOR_SPACE_XYZ_D55_2: + return XYZ::get(IO::getIOs(D55_2)); + break; + case cv::ccm::COLOR_SPACE_XYZ_D55_10: + return XYZ::get(IO::getIOs(D55_10)); + break; + case cv::ccm::COLOR_SPACE_XYZ_D75_2: + return XYZ::get(IO::getIOs(D75_2)); + break; + case cv::ccm::COLOR_SPACE_XYZ_D75_10: + return XYZ::get(IO::getIOs(D75_10)); + break; + case cv::ccm::COLOR_SPACE_XYZ_E_2: + return XYZ::get(IO::getIOs(E_2)); + break; + case cv::ccm::COLOR_SPACE_XYZ_E_10: + return XYZ::get(IO::getIOs(E_10)); + break; + case cv::ccm::COLOR_SPACE_Lab_D65_2: + return Lab::get(IO::getIOs(D65_2)); + break; + case cv::ccm::COLOR_SPACE_Lab_D50_2: + return Lab::get(IO::getIOs(D50_2)); + break; + case cv::ccm::COLOR_SPACE_Lab_D65_10: + return Lab::get(IO::getIOs(D65_10)); + break; + case cv::ccm::COLOR_SPACE_Lab_D50_10: + return Lab::get(IO::getIOs(D50_10)); + break; + case cv::ccm::COLOR_SPACE_Lab_A_2: + return Lab::get(IO::getIOs(A_2)); + break; + case cv::ccm::COLOR_SPACE_Lab_A_10: + return Lab::get(IO::getIOs(A_10)); + break; + case cv::ccm::COLOR_SPACE_Lab_D55_2: + return Lab::get(IO::getIOs(D55_2)); + break; + case cv::ccm::COLOR_SPACE_Lab_D55_10: + return Lab::get(IO::getIOs(D55_10)); + break; + case cv::ccm::COLOR_SPACE_Lab_D75_2: + return Lab::get(IO::getIOs(D75_2)); + break; + case cv::ccm::COLOR_SPACE_Lab_D75_10: + return Lab::get(IO::getIOs(D75_10)); + break; + case cv::ccm::COLOR_SPACE_Lab_E_2: + return Lab::get(IO::getIOs(E_2)); + break; + case cv::ccm::COLOR_SPACE_Lab_E_10: + return Lab::get(IO::getIOs(E_10)); + break; + default: + break; + } + + return map_cs[cs_name]; +} + +} +} // namespace cv::ccm diff --git a/modules/mcc/src/colorspace.hpp b/modules/mcc/src/colorspace.hpp new file mode 100644 index 00000000000..57b5bc2ff40 --- /dev/null +++ b/modules/mcc/src/colorspace.hpp @@ -0,0 +1,365 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_COLORSPACE_HPP__ +#define __OPENCV_MCC_COLORSPACE_HPP__ + +#include "operations.hpp" +#include "io.hpp" +#include "opencv2/mcc/ccm.hpp" + +namespace cv { +namespace ccm { + +/** @brief Basic class for ColorSpace. +*/ +class ColorSpace +{ +public: + typedef std::function MatFunc; + IO io; + std::string type; + bool linear; + Operations to; + Operations from; + ColorSpace* l; + ColorSpace* nl; + + ColorSpace() {}; + + ColorSpace(IO io_, std::string type_, bool linear_) + : io(io_) + , type(type_) + , linear(linear_) {}; + + virtual ~ColorSpace() + { + l = 0; + nl = 0; + }; + virtual bool relate(const ColorSpace& other) const; + + virtual Operations relation(const ColorSpace& /*other*/) const; + + bool operator<(const ColorSpace& other) const; +}; + +/** @brief Base of RGB color space; + the argument values are from AdobeRGB; + Data from https://en.wikipedia.org/wiki/Adobe_RGB_color_space +*/ + +class RGBBase_ : public ColorSpace +{ +public: + // primaries + double xr; + double yr; + double xg; + double yg; + double xb; + double yb; + MatFunc toL; + MatFunc fromL; + Mat M_to; + Mat M_from; + + using ColorSpace::ColorSpace; + + /** @brief There are 3 kinds of relationships for RGB: + 1. Different types; - no operation + 1. Same type, same linear; - copy + 2. Same type, different linear, self is nonlinear; - 2 toL + 3. Same type, different linear, self is linear - 3 fromL + @param other type of ColorSpace. + @return Operations. + */ + Operations relation(const ColorSpace& other) const CV_OVERRIDE; + + /** @brief Initial operations. + */ + void init(); + /** @brief Produce color space instance with linear and non-linear versions. + @param rgbl type of RGBBase_. + */ + void bind(RGBBase_& rgbl); + +private: + virtual void setParameter() {}; + + /** @brief Calculation of M_RGBL2XYZ_base. + */ + virtual void calM(); + + /** @brief operations to or from XYZ. + */ + virtual void calOperations(); + + virtual void calLinear() {}; + + virtual Mat toLFunc(Mat& /*rgb*/); + + virtual Mat fromLFunc(Mat& /*rgbl*/); +}; + +/** @brief Base of Adobe RGB color space; +*/ +class AdobeRGBBase_ : public RGBBase_ + +{ +public: + using RGBBase_::RGBBase_; + double gamma; + +private: + Mat toLFunc(Mat& rgb) CV_OVERRIDE; + Mat fromLFunc(Mat& rgbl) CV_OVERRIDE; +}; + +/** @brief Base of sRGB color space; +*/ +class sRGBBase_ : public RGBBase_ + +{ +public: + using RGBBase_::RGBBase_; + double a; + double gamma; + double alpha; + double beta; + double phi; + double K0; + +private: + /** @brief linearization parameters + */ + virtual void calLinear() CV_OVERRIDE; + /** @brief Used by toLFunc. + */ + double toLFuncEW(double& x); + + /** @brief Linearization. + @param rgb the input array, type of cv::Mat. + @return the output array, type of cv::Mat. + */ + Mat toLFunc(Mat& rgb) CV_OVERRIDE; + + /** @brief Used by fromLFunc. + */ + double fromLFuncEW(double& x); + + /** @brief Delinearization. + @param rgbl the input array, type of cv::Mat. + @return the output array, type of cv::Mat. + */ + Mat fromLFunc(Mat& rgbl) CV_OVERRIDE; +}; + +/** @brief sRGB color space. + data from https://en.wikipedia.org/wiki/SRGB. +*/ +class sRGB_ : public sRGBBase_ + +{ +public: + sRGB_(bool linear_) + : sRGBBase_(IO::getIOs(D65_2), "sRGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief Adobe RGB color space. +*/ +class AdobeRGB_ : public AdobeRGBBase_ +{ +public: + AdobeRGB_(bool linear_ = false) + : AdobeRGBBase_(IO::getIOs(D65_2), "AdobeRGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief Wide-gamut RGB color space. + data from https://en.wikipedia.org/wiki/Wide-gamut_RGB_color_space. +*/ +class WideGamutRGB_ : public AdobeRGBBase_ +{ +public: + WideGamutRGB_(bool linear_ = false) + : AdobeRGBBase_(IO::getIOs(D50_2), "WideGamutRGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief ProPhoto RGB color space. + data from https://en.wikipedia.org/wiki/ProPhoto_RGB_color_space. +*/ + +class ProPhotoRGB_ : public AdobeRGBBase_ +{ +public: + ProPhotoRGB_(bool linear_ = false) + : AdobeRGBBase_(IO::getIOs(D50_2), "ProPhotoRGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief DCI-P3 RGB color space. + data from https://en.wikipedia.org/wiki/DCI-P3. +*/ +class DCI_P3_RGB_ : public AdobeRGBBase_ +{ +public: + DCI_P3_RGB_(bool linear_ = false) + : AdobeRGBBase_(IO::getIOs(D65_2), "DCI_P3_RGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief Apple RGB color space. + data from http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html. +*/ +class AppleRGB_ : public AdobeRGBBase_ +{ +public: + AppleRGB_(bool linear_ = false) + : AdobeRGBBase_(IO::getIOs(D65_2), "AppleRGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief REC_709 RGB color space. + data from https://en.wikipedia.org/wiki/Rec._709. +*/ +class REC_709_RGB_ : public sRGBBase_ +{ +public: + REC_709_RGB_(bool linear_) + : sRGBBase_(IO::getIOs(D65_2), "REC_709_RGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief REC_2020 RGB color space. + data from https://en.wikipedia.org/wiki/Rec._2020. +*/ +class REC_2020_RGB_ : public sRGBBase_ +{ +public: + REC_2020_RGB_(bool linear_) + : sRGBBase_(IO::getIOs(D65_2), "REC_2020_RGB", linear_) {}; + +private: + void setParameter() CV_OVERRIDE; +}; + +/** @brief Enum of the possible types of CAMs. +*/ +enum CAM +{ + IDENTITY, + VON_KRIES, + BRADFORD +}; + + +/** @brief XYZ color space. + Chromatic adaption matrices. +*/ +class XYZ : public ColorSpace +{ +public: + XYZ(IO io_) + : ColorSpace(io_, "XYZ", true) {}; + Operations cam(IO dio, CAM method = BRADFORD); + static std::shared_ptr get(IO io); + +private: + /** @brief Get cam. + @param sio the input IO of src. + @param dio the input IO of dst. + @param method type of CAM. + @return the output array, type of cv::Mat. + */ + Mat cam_(IO sio, IO dio, CAM method = BRADFORD) const; +}; + +/** @brief Lab color space. +*/ +class Lab : public ColorSpace +{ +public: + Lab(IO io); + static std::shared_ptr get(IO io); + +private: + static constexpr double delta = (6. / 29.); + static constexpr double m = 1. / (3. * delta * delta); + static constexpr double t0 = delta * delta * delta; + static constexpr double c = 4. / 29.; + + Vec3d fromxyz(Vec3d& xyz); + + /** @brief Calculate From. + @param src the input array, type of cv::Mat. + @return the output array, type of cv::Mat + */ + Mat fromsrc(Mat& src); + + Vec3d tolab(Vec3d& lab); + + /** @brief Calculate To. + @param src the input array, type of cv::Mat. + @return the output array, type of cv::Mat + */ + Mat tosrc(Mat& src); +}; + +class GetCS +{ +protected: + std::map> map_cs; + + GetCS(); // singleton, use getInstance() +public: + static GetCS& getInstance(); + + std::shared_ptr get_rgb(enum COLOR_SPACE cs_name); + std::shared_ptr get_cs(enum COLOR_SPACE cs_name); +}; + +} +} // namespace cv::ccm + +#endif \ No newline at end of file diff --git a/modules/mcc/src/distance.cpp b/modules/mcc/src/distance.cpp new file mode 100644 index 00000000000..7996379e939 --- /dev/null +++ b/modules/mcc/src/distance.cpp @@ -0,0 +1,222 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "distance.hpp" + +namespace cv { +namespace ccm { + +double deltaCIE76(const Vec3d& lab1, const Vec3d& lab2) { return norm(lab1 - lab2); }; + +double deltaCIE94(const Vec3d& lab1, const Vec3d& lab2, const double& kH, + const double& kC, const double& kL, const double& k1, const double& k2) +{ + double dl = lab1[0] - lab2[0]; + double c1 = sqrt(pow(lab1[1], 2) + pow(lab1[2], 2)); + double c2 = sqrt(pow(lab2[1], 2) + pow(lab2[2], 2)); + double dc = c1 - c2; + double da = lab1[1] - lab2[1]; + double db = lab1[2] - lab2[2]; + double dh = pow(da, 2) + pow(db, 2) - pow(dc, 2); + double sc = 1.0 + k1 * c1; + double sh = 1.0 + k2 * c1; + double sl = 1.0; + double res = pow(dl / (kL * sl), 2) + pow(dc / (kC * sc), 2) + dh / pow(kH * sh, 2); + + return res > 0 ? sqrt(res) : 0; +} + +double deltaCIE94GraphicArts(const Vec3d& lab1, const Vec3d& lab2) +{ + return deltaCIE94(lab1, lab2); +} + +double toRad(const double& degree) { return degree / 180 * CV_PI; }; + +double deltaCIE94Textiles(const Vec3d& lab1, const Vec3d& lab2) +{ + return deltaCIE94(lab1, lab2, 1.0, 1.0, 2.0, 0.048, 0.014); +} + +double deltaCIEDE2000_(const Vec3d& lab1, const Vec3d& lab2, const double& kL, + const double& kC, const double& kH) +{ + double delta_L_apo = lab2[0] - lab1[0]; + double l_bar_apo = (lab1[0] + lab2[0]) / 2.0; + double C1 = sqrt(pow(lab1[1], 2) + pow(lab1[2], 2)); + double C2 = sqrt(pow(lab2[1], 2) + pow(lab2[2], 2)); + double C_bar = (C1 + C2) / 2.0; + double G = sqrt(pow(C_bar, 7) / (pow(C_bar, 7) + pow(25, 7))); + double a1_apo = lab1[1] + lab1[1] / 2.0 * (1.0 - G); + double a2_apo = lab2[1] + lab2[1] / 2.0 * (1.0 - G); + double C1_apo = sqrt(pow(a1_apo, 2) + pow(lab1[2], 2)); + double C2_apo = sqrt(pow(a2_apo, 2) + pow(lab2[2], 2)); + double C_bar_apo = (C1_apo + C2_apo) / 2.0; + double delta_C_apo = C2_apo - C1_apo; + + double h1_apo; + if (C1_apo == 0) + { + h1_apo = 0.0; + } + else + { + h1_apo = atan2(lab1[2], a1_apo); + if (h1_apo < 0.0) + h1_apo += 2. * CV_PI; + } + + double h2_apo; + if (C2_apo == 0) + { + h2_apo = 0.0; + } + else + { + h2_apo = atan2(lab2[2], a2_apo); + if (h2_apo < 0.0) + h2_apo += 2. * CV_PI; + } + + double delta_h_apo; + if (abs(h2_apo - h1_apo) <= CV_PI) + { + delta_h_apo = h2_apo - h1_apo; + } + else if (h2_apo <= h1_apo) + { + delta_h_apo = h2_apo - h1_apo + 2. * CV_PI; + } + else + { + delta_h_apo = h2_apo - h1_apo - 2. * CV_PI; + } + + double H_bar_apo; + if (C1_apo == 0 || C2_apo == 0) + { + H_bar_apo = h1_apo + h2_apo; + } + else if (abs(h1_apo - h2_apo) <= CV_PI) + { + H_bar_apo = (h1_apo + h2_apo) / 2.0; + } + else if (h1_apo + h2_apo < 2. * CV_PI) + { + H_bar_apo = (h1_apo + h2_apo + 2. * CV_PI) / 2.0; + } + else + { + H_bar_apo = (h1_apo + h2_apo - 2. * CV_PI) / 2.0; + } + + double delta_H_apo = 2.0 * sqrt(C1_apo * C2_apo) * sin(delta_h_apo / 2.0); + double T = 1.0 - 0.17 * cos(H_bar_apo - toRad(30.)) + 0.24 * cos(2.0 * H_bar_apo) + 0.32 * cos(3.0 * H_bar_apo + toRad(6.0)) - 0.2 * cos(4.0 * H_bar_apo - toRad(63.0)); + double sC = 1.0 + 0.045 * C_bar_apo; + double sH = 1.0 + 0.015 * C_bar_apo * T; + double sL = 1.0 + ((0.015 * pow(l_bar_apo - 50.0, 2.0)) / sqrt(20.0 + pow(l_bar_apo - 50.0, 2.0))); + double RT = -2.0 * G * sin(toRad(60.0) * exp(-pow((H_bar_apo - toRad(275.0)) / toRad(25.0), 2.0))); + double res = (pow(delta_L_apo / (kL * sL), 2.0) + pow(delta_C_apo / (kC * sC), 2.0) + pow(delta_H_apo / (kH * sH), 2.0) + RT * (delta_C_apo / (kC * sC)) * (delta_H_apo / (kH * sH))); + return res > 0 ? sqrt(res) : 0; +} + +double deltaCIEDE2000(const Vec3d& lab1, const Vec3d& lab2) +{ + return deltaCIEDE2000_(lab1, lab2); +} + +double deltaCMC(const Vec3d& lab1, const Vec3d& lab2, const double& kL, const double& kC) +{ + double dL = lab2[0] - lab1[0]; + double da = lab2[1] - lab1[1]; + double db = lab2[2] - lab1[2]; + double C1 = sqrt(pow(lab1[1], 2.0) + pow(lab1[2], 2.0)); + double C2 = sqrt(pow(lab2[1], 2.0) + pow(lab2[2], 2.0)); + double dC = C2 - C1; + double dH = sqrt(pow(da, 2) + pow(db, 2) - pow(dC, 2)); + + double H1; + if (C1 == 0.) + { + H1 = 0.0; + } + else + { + H1 = atan2(lab1[2], lab1[1]); + if (H1 < 0.0) + H1 += 2. * CV_PI; + } + + double F = pow(C1, 2) / sqrt(pow(C1, 4) + 1900); + double T = (H1 > toRad(164) && H1 <= toRad(345)) + ? 0.56 + abs(0.2 * cos(H1 + toRad(168))) + : 0.36 + abs(0.4 * cos(H1 + toRad(35))); + double sL = lab1[0] < 16. ? 0.511 : (0.040975 * lab1[0]) / (1.0 + 0.01765 * lab1[0]); + double sC = (0.0638 * C1) / (1.0 + 0.0131 * C1) + 0.638; + double sH = sC * (F * T + 1.0 - F); + + return sqrt(pow(dL / (kL * sL), 2.0) + pow(dC / (kC * sC), 2.0) + pow(dH / sH, 2.0)); +} + +double deltaCMC1To1(const Vec3d& lab1, const Vec3d& lab2) +{ + return deltaCMC(lab1, lab2); +} + +double deltaCMC2To1(const Vec3d& lab1, const Vec3d& lab2) +{ + return deltaCMC(lab1, lab2, 2, 1); +} + +Mat distance(Mat src, Mat ref, DISTANCE_TYPE distance_type) +{ + switch (distance_type) + { + case cv::ccm::DISTANCE_CIE76: + return distanceWise(src, ref, deltaCIE76); + case cv::ccm::DISTANCE_CIE94_GRAPHIC_ARTS: + return distanceWise(src, ref, deltaCIE94GraphicArts); + case cv::ccm::DISTANCE_CIE94_TEXTILES: + return distanceWise(src, ref, deltaCIE94Textiles); + case cv::ccm::DISTANCE_CIE2000: + return distanceWise(src, ref, deltaCIEDE2000); + case cv::ccm::DISTANCE_CMC_1TO1: + return distanceWise(src, ref, deltaCMC1To1); + case cv::ccm::DISTANCE_CMC_2TO1: + return distanceWise(src, ref, deltaCMC2To1); + case cv::ccm::DISTANCE_RGB: + return distanceWise(src, ref, deltaCIE76); + case cv::ccm::DISTANCE_RGBL: + return distanceWise(src, ref, deltaCIE76); + default: + CV_Error(Error::StsBadArg, "Wrong distance_type!" ); + break; + } +}; + +} +} // namespace ccm \ No newline at end of file diff --git a/modules/mcc/src/distance.hpp b/modules/mcc/src/distance.hpp new file mode 100644 index 00000000000..5acfc93cdb6 --- /dev/null +++ b/modules/mcc/src/distance.hpp @@ -0,0 +1,99 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_DISTANCE_HPP__ +#define __OPENCV_MCC_DISTANCE_HPP__ + +#include "utils.hpp" +#include "opencv2/mcc/ccm.hpp" + +namespace cv { +namespace ccm { +/** possibale functions to calculate the distance between + colors.see https://en.wikipedia.org/wiki/Color_difference for details;*/ + +/** @brief distance between two points in formula CIE76 + @param lab1 a 3D vector + @param lab2 a 3D vector + @return distance between lab1 and lab2 +*/ + +double deltaCIE76(const Vec3d& lab1, const Vec3d& lab2); + +/** @brief distance between two points in formula CIE94 + @param lab1 a 3D vector + @param lab2 a 3D vector + @param kH Hue scale + @param kC Chroma scale + @param kL Lightness scale + @param k1 first scale parameter + @param k2 second scale parameter + @return distance between lab1 and lab2 +*/ + +double deltaCIE94(const Vec3d& lab1, const Vec3d& lab2, const double& kH = 1.0, + const double& kC = 1.0, const double& kL = 1.0, const double& k1 = 0.045, + const double& k2 = 0.015); + +double deltaCIE94GraphicArts(const Vec3d& lab1, const Vec3d& lab2); + +double toRad(const double& degree); + +double deltaCIE94Textiles(const Vec3d& lab1, const Vec3d& lab2); + +/** @brief distance between two points in formula CIE2000 + @param lab1 a 3D vector + @param lab2 a 3D vector + @param kL Lightness scale + @param kC Chroma scale + @param kH Hue scale + @return distance between lab1 and lab2 +*/ +double deltaCIEDE2000_(const Vec3d& lab1, const Vec3d& lab2, const double& kL = 1.0, + const double& kC = 1.0, const double& kH = 1.0); +double deltaCIEDE2000(const Vec3d& lab1, const Vec3d& lab2); + +/** @brief distance between two points in formula CMC + @param lab1 a 3D vector + @param lab2 a 3D vector + @param kL Lightness scale + @param kC Chroma scale + @return distance between lab1 and lab2 +*/ + +double deltaCMC(const Vec3d& lab1, const Vec3d& lab2, const double& kL = 1, const double& kC = 1); + +double deltaCMC1To1(const Vec3d& lab1, const Vec3d& lab2); + +double deltaCMC2To1(const Vec3d& lab1, const Vec3d& lab2); + +Mat distance(Mat src,Mat ref, DISTANCE_TYPE distance_type); + +} +} // namespace cv::ccm + +#endif \ No newline at end of file diff --git a/modules/mcc/src/io.cpp b/modules/mcc/src/io.cpp new file mode 100644 index 00000000000..c9c5c2026aa --- /dev/null +++ b/modules/mcc/src/io.cpp @@ -0,0 +1,133 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "io.hpp" +namespace cv { +namespace ccm { +IO::IO(std::string illuminant_, std::string observer_) + : illuminant(illuminant_) + , observer(observer_) {}; + +bool IO::operator<(const IO& other) const +{ + return (illuminant < other.illuminant || ((illuminant == other.illuminant) && (observer < other.observer))); +} + +bool IO::operator==(const IO& other) const +{ + return illuminant == other.illuminant && observer == other.observer; +}; + +IO IO::getIOs(IO_TYPE io) +{ + switch (io) + { + case cv::ccm::A_2: + { + IO A_2_IO("A", "2"); + return A_2_IO; + break; + } + case cv::ccm::A_10: + { + IO A_1O_IO("A", "10"); + return A_1O_IO; + break; + } + case cv::ccm::D50_2: + { + IO D50_2_IO("D50", "2"); + return D50_2_IO; + break; + } + case cv::ccm::D50_10: + { + IO D50_10_IO("D50", "10"); + return D50_10_IO; + break; + } + case cv::ccm::D55_2: + { + IO D55_2_IO("D55", "2"); + return D55_2_IO; + break; + } + case cv::ccm::D55_10: + { + IO D55_10_IO("D55", "10"); + return D55_10_IO; + break; + } + case cv::ccm::D65_2: + { + IO D65_2_IO("D65", "2"); + return D65_2_IO; + } + case cv::ccm::D65_10: + { + IO D65_10_IO("D65", "10"); + return D65_10_IO; + break; + } + case cv::ccm::D75_2: + { + IO D75_2_IO("D75", "2"); + return D75_2_IO; + break; + } + case cv::ccm::D75_10: + { + IO D75_10_IO("D75", "10"); + return D75_10_IO; + break; + } + case cv::ccm::E_2: + { + IO E_2_IO("E", "2"); + return E_2_IO; + break; + } + case cv::ccm::E_10: + { + IO E_10_IO("E", "10"); + return E_10_IO; + break; + } + default: + return IO(); + break; + } +} +// data from https://en.wikipedia.org/wiki/Standard_illuminant. +std::vector xyY2XYZ(const std::vector& xyY) +{ + double Y = xyY.size() >= 3 ? xyY[2] : 1; + return { Y * xyY[0] / xyY[1], Y, Y / xyY[1] * (1 - xyY[0] - xyY[1]) }; +} + +} +} // namespace cv::ccm diff --git a/modules/mcc/src/io.hpp b/modules/mcc/src/io.hpp new file mode 100644 index 00000000000..c79864e3c41 --- /dev/null +++ b/modules/mcc/src/io.hpp @@ -0,0 +1,72 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_IO_HPP__ +#define __OPENCV_MCC_IO_HPP__ + +#include +#include + +namespace cv { +namespace ccm { + +enum IO_TYPE +{ + A_2, + A_10, + D50_2, + D50_10, + D55_2, + D55_10, + D65_2, + D65_10, + D75_2, + D75_10, + E_2, + E_10 +}; + +/** @brief Io is the meaning of illuminant and observer. See notes of ccm.hpp + for supported list for illuminant and observer*/ +class IO +{ +public: + std::string illuminant; + std::string observer; + IO() {}; + IO(std::string illuminant, std::string observer); + virtual ~IO() {}; + bool operator<(const IO& other) const; + bool operator==(const IO& other) const; + static IO getIOs(IO_TYPE io); +}; +std::vector xyY2XYZ(const std::vector& xyY); + +} +} // namespace cv::ccm + +#endif \ No newline at end of file diff --git a/modules/mcc/src/linearize.cpp b/modules/mcc/src/linearize.cpp new file mode 100644 index 00000000000..3a48a560eb8 --- /dev/null +++ b/modules/mcc/src/linearize.cpp @@ -0,0 +1,130 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "linearize.hpp" + +namespace cv { +namespace ccm { + +Polyfit::Polyfit(Mat x, Mat y, int deg_) + : deg(deg_) +{ + int n = x.cols * x.rows * x.channels(); + x = x.reshape(1, n); + y = y.reshape(1, n); + Mat_ A = Mat_::ones(n, deg + 1); + for (int i = 0; i < n; ++i) + { + for (int j = 1; j < A.cols; ++j) + { + A.at(i, j) = x.at(i) * A.at(i, j - 1); + } + } + Mat y_(y); + cv::solve(A, y_, p, DECOMP_SVD); +} + +Mat Polyfit::operator()(const Mat& inp) +{ + return elementWise(inp, [this](double x) -> double { return fromEW(x); }); +}; + +double Polyfit::fromEW(double x) +{ + double res = 0; + for (int d = 0; d <= deg; ++d) + { + res += pow(x, d) * p.at(d, 0); + } + return res; +}; + +LogPolyfit::LogPolyfit(Mat x, Mat y, int deg_) + : deg(deg_) +{ + Mat mask_ = (x > 0) & (y > 0); + Mat src_, dst_, s_, d_; + src_ = maskCopyTo(x, mask_); + dst_ = maskCopyTo(y, mask_); + log(src_, s_); + log(dst_, d_); + p = Polyfit(s_, d_, deg); +} + +Mat LogPolyfit::operator()(const Mat& inp) +{ + Mat mask_ = inp >= 0; + Mat y, y_, res; + log(inp, y); + y = p(y); + exp(y, y_); + y_.copyTo(res, mask_); + return res; +}; + +Mat Linear::linearize(Mat inp) +{ + return inp; +}; + +Mat LinearGamma::linearize(Mat inp) +{ + return gammaCorrection(inp, gamma); +}; + +std::shared_ptr getLinear(double gamma, int deg, Mat src, Color dst, Mat mask, RGBBase_ cs, LINEAR_TYPE linear_type) +{ + std::shared_ptr p = std::make_shared(); + switch (linear_type) + { + case cv::ccm::LINEARIZATION_IDENTITY: + p.reset(new LinearIdentity()); + break; + case cv::ccm::LINEARIZATION_GAMMA: + p.reset(new LinearGamma(gamma)); + break; + case cv::ccm::LINEARIZATION_COLORPOLYFIT: + p.reset(new LinearColor(deg, src, dst, mask, cs)); + break; + case cv::ccm::LINEARIZATION_COLORLOGPOLYFIT: + p.reset(new LinearColor(deg, src, dst, mask, cs)); + break; + case cv::ccm::LINEARIZATION_GRAYPOLYFIT: + p.reset(new LinearGray(deg, src, dst, mask, cs)); + break; + case cv::ccm::LINEARIZATION_GRAYLOGPOLYFIT: + p.reset(new LinearGray(deg, src, dst, mask, cs)); + break; + default: + CV_Error(Error::StsBadArg, "Wrong linear_type!" ); + break; + } + return p; +}; + +} +} // namespace cv::ccm \ No newline at end of file diff --git a/modules/mcc/src/linearize.hpp b/modules/mcc/src/linearize.hpp new file mode 100644 index 00000000000..a703b5c5293 --- /dev/null +++ b/modules/mcc/src/linearize.hpp @@ -0,0 +1,209 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_LINEARIZE_HPP__ +#define __OPENCV_MCC_LINEARIZE_HPP__ + +#include +#include +#include "color.hpp" +#include "opencv2/mcc/ccm.hpp" +namespace cv { +namespace ccm { + +/** @brief Polyfit model. +*/ +class Polyfit +{ +public: + int deg; + Mat p; + Polyfit() {}; + + /** @brief Polyfit method. + https://en.wikipedia.org/wiki/Polynomial_regression + polynomial: yi = a0 + a1*xi + a2*xi^2 + ... + an*xi^deg (i = 1,2,...,n) + and deduct: Ax = y + */ + Polyfit(Mat x, Mat y, int deg); + virtual ~Polyfit() {}; + Mat operator()(const Mat& inp); + +private: + double fromEW(double x); +}; + +/** @brief Logpolyfit model. +*/ +class LogPolyfit + +{ +public: + int deg; + Polyfit p; + + LogPolyfit() {}; + + /** @brief Logpolyfit method. + */ + LogPolyfit(Mat x, Mat y, int deg); + virtual ~LogPolyfit() {}; + Mat operator()(const Mat& inp); +}; + +/** @brief Linearization base. +*/ + +class Linear +{ +public: + Linear() {}; + virtual ~Linear() {}; + + /** @brief Inference. + @param inp the input array, type of cv::Mat. + */ + virtual Mat linearize(Mat inp); + /* *\brief Evaluate linearization model. + */ + virtual void value(void) {}; +}; + +/** @brief Linearization identity. + make no change. +*/ +class LinearIdentity : public Linear +{}; + +/** @brief Linearization gamma correction. +*/ +class LinearGamma : public Linear +{ +public: + double gamma; + + LinearGamma(double gamma_) + : gamma(gamma_) {}; + + Mat linearize(Mat inp) CV_OVERRIDE; +}; + +/** @brief Linearization. + Grayscale polynomial fitting. +*/ +template +class LinearGray : public Linear +{ +public: + int deg; + T p; + LinearGray(int deg_, Mat src, Color dst, Mat mask, RGBBase_ cs) + : deg(deg_) + { + dst.getGray(); + Mat lear_gray_mask = mask & dst.grays; + + // the grayscale function is approximate for src is in relative color space. + src = rgb2gray(maskCopyTo(src, lear_gray_mask)); + Mat dst_ = maskCopyTo(dst.toGray(cs.io), lear_gray_mask); + calc(src, dst_); + } + + /** @brief monotonically increase is not guaranteed. + @param src the input array, type of cv::Mat. + @param dst the input array, type of cv::Mat. + */ + void calc(const Mat& src, const Mat& dst) + { + p = T(src, dst, deg); + }; + + Mat linearize(Mat inp) CV_OVERRIDE + { + return p(inp); + }; +}; + +/** @brief Linearization. + Fitting channels respectively. +*/ +template +class LinearColor : public Linear +{ +public: + int deg; + T pr; + T pg; + T pb; + + LinearColor(int deg_, Mat src_, Color dst, Mat mask, RGBBase_ cs) + : deg(deg_) + { + Mat src = maskCopyTo(src_, mask); + Mat dst_ = maskCopyTo(dst.to(*cs.l).colors, mask); + calc(src, dst_); + } + + void calc(const Mat& src, const Mat& dst) + { + Mat schannels[3]; + Mat dchannels[3]; + split(src, schannels); + split(dst, dchannels); + pr = T(schannels[0], dchannels[0], deg); + pg = T(schannels[1], dchannels[1], deg); + pb = T(schannels[2], dchannels[2], deg); + }; + + Mat linearize(Mat inp) CV_OVERRIDE + { + Mat channels[3]; + split(inp, channels); + std::vector channel; + Mat res; + merge(std::vector { pr(channels[0]), pg(channels[1]), pb(channels[2]) }, res); + return res; + }; +}; + +/** @brief Get linearization method. + used in ccm model. + @param gamma used in LinearGamma. + @param deg degrees. + @param src the input array, type of cv::Mat. + @param dst the input array, type of cv::Mat. + @param mask the input array, type of cv::Mat. + @param cs type of RGBBase_. + @param linear_type type of linear. +*/ + +std::shared_ptr getLinear(double gamma, int deg, Mat src, Color dst, Mat mask, RGBBase_ cs, LINEAR_TYPE linear_type); + +} +} // namespace cv::ccm + +#endif diff --git a/modules/mcc/src/operations.cpp b/modules/mcc/src/operations.cpp new file mode 100644 index 00000000000..e4e76a2a270 --- /dev/null +++ b/modules/mcc/src/operations.cpp @@ -0,0 +1,90 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "operations.hpp" +#include "utils.hpp" +namespace cv { +namespace ccm { + +Mat Operation::operator()(Mat& abc) +{ + if (!linear) + { + return f(abc); + } + if (M.empty()) + { + return abc; + } + return multiple(abc, M); +}; + +void Operation::add(const Operation& other) +{ + if (M.empty()) + { + M = other.M.clone(); + } + else + { + M = M * other.M; + } +}; + +void Operation::clear() +{ + M = Mat(); +}; + +Operations& Operations::add(const Operations& other) +{ + ops.insert(ops.end(), other.ops.begin(), other.ops.end()); + return *this; +}; + +Mat Operations::run(Mat abc) +{ + Operation hd; + for (auto& op : ops) + { + if (op.linear) + { + hd.add(op); + } + else + { + abc = hd(abc); + hd.clear(); + abc = op(abc); + } + } + abc = hd(abc); + return abc; +}; + +} +} // namespace cv::ccm \ No newline at end of file diff --git a/modules/mcc/src/operations.hpp b/modules/mcc/src/operations.hpp new file mode 100644 index 00000000000..ae3b39b6019 --- /dev/null +++ b/modules/mcc/src/operations.hpp @@ -0,0 +1,102 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_OPERATIONS_HPP__ +#define __OPENCV_MCC_OPERATIONS_HPP__ + +#include "utils.hpp" + +namespace cv { +namespace ccm { + +/** @brief Operation class contains some operarions used for color space + conversion containing linear transformation and non-linear transformation + */ +class Operation +{ +public: + typedef std::function MatFunc; + bool linear; + Mat M; + MatFunc f; + + Operation() + : linear(true) + , M(Mat()) {}; + Operation(Mat M_) + : linear(true) + , M(M_) {}; + Operation(MatFunc f_) + : linear(false) + , f(f_) {}; + virtual ~Operation() {}; + + /** @brief operator function will run operation + */ + Mat operator()(Mat& abc); + + /** @brief add function will conbine this operation + with other linear transformation operation + */ + void add(const Operation& other); + + void clear(); + static Operation& get_IDENTITY_OP() + { + static Operation identity_op([](Mat x) { return x; }); + return identity_op; + } +}; + +class Operations +{ +public: + std::vector ops; + Operations() + : ops {} {}; + Operations(std::initializer_list op) + : ops { op } {}; + virtual ~Operations() {}; + + /** @brief add function will conbine this operation with other transformation operations + */ + Operations& add(const Operations& other); + + /** @brief run operations to make color conversion + */ + Mat run(Mat abc); + static const Operations& get_IDENTITY_OPS() + { + static Operations Operation_op {Operation::get_IDENTITY_OP()}; + return Operation_op; + } +}; + +} +} // namespace cv::ccm + +#endif \ No newline at end of file diff --git a/modules/mcc/src/utils.cpp b/modules/mcc/src/utils.cpp new file mode 100644 index 00000000000..3a0128b6ef6 --- /dev/null +++ b/modules/mcc/src/utils.cpp @@ -0,0 +1,119 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#include "utils.hpp" + +namespace cv { +namespace ccm { + +double gammaCorrection_(const double& element, const double& gamma) +{ + return (element >= 0 ? pow(element, gamma) : -pow((-element), gamma)); +} + +Mat gammaCorrection(const Mat& src, const double& gamma) +{ + return elementWise(src, [gamma](double element) -> double { return gammaCorrection_(element, gamma); }); +} + +Mat maskCopyTo(const Mat& src, const Mat& mask) +{ + Mat dst(countNonZero(mask), 1, src.type()); + const int channel = src.channels(); + auto it_mask = mask.begin(); + switch (channel) + { + case 1: + { + auto it_src = src.begin(), end_src = src.end(); + auto it_dst = dst.begin(); + for (; it_src != end_src; ++it_src, ++it_mask) + { + if (*it_mask) + { + (*it_dst) = (*it_src); + ++it_dst; + } + } + break; + } + case 3: + { + auto it_src = src.begin(), end_src = src.end(); + auto it_dst = dst.begin(); + for (; it_src != end_src; ++it_src, ++it_mask) + { + if (*it_mask) + { + (*it_dst) = (*it_src); + ++it_dst; + } + } + break; + } + default: + CV_Error(Error::StsBadArg, "Wrong channel!" ); + break; + } + return dst; +} + +Mat multiple(const Mat& xyz, const Mat& ccm) +{ + Mat tmp = xyz.reshape(1, xyz.rows * xyz.cols); + Mat res = tmp * ccm; + res = res.reshape(res.cols, xyz.rows); + return res; +} + +Mat saturate(Mat& src, const double& low, const double& up) +{ + Mat dst = Mat::ones(src.size(), CV_8UC1); + MatIterator_ it_src = src.begin(), end_src = src.end(); + MatIterator_ it_dst = dst.begin(); + for (; it_src != end_src; ++it_src, ++it_dst) + { + for (int i = 0; i < 3; ++i) + { + if ((*it_src)[i] > up || (*it_src)[i] < low) + { + *it_dst = 0; + break; + } + } + } + return dst; +} + +Mat rgb2gray(const Mat& rgb) +{ + const Matx31d m_gray(0.2126, 0.7152, 0.0722); + return multiple(rgb, Mat(m_gray)); +} + +} +} // namespace cv::ccm diff --git a/modules/mcc/src/utils.hpp b/modules/mcc/src/utils.hpp new file mode 100644 index 00000000000..02570ca0184 --- /dev/null +++ b/modules/mcc/src/utils.hpp @@ -0,0 +1,155 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright(C) 2020, Huawei Technologies Co.,Ltd. All rights reserved. +// Third party copyrights are property of their respective owners. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Longbu Wang +// Jinheng Zhang +// Chenqi Shan + +#ifndef __OPENCV_MCC_UTILS_HPP__ +#define __OPENCV_MCC_UTILS_HPP__ + +#include + +namespace cv { +namespace ccm { + +double gammaCorrection_(const double& element, const double& gamma); + +/** @brief gamma correction. + \f[ + C_l=C_n^{\gamma},\qquad C_n\ge0\\ + C_l=-(-C_n)^{\gamma},\qquad C_n<0\\\\ + \f] + @param src the input array,type of Mat. + @param gamma a constant for gamma correction. + */ +Mat gammaCorrection(const Mat& src, const double& gamma); + +/** @brief maskCopyTo a function to delete unsatisfied elementwise. + @param src the input array, type of Mat. + @param mask operation mask that used to choose satisfided elementwise. + */ +Mat maskCopyTo(const Mat& src, const Mat& mask); + +/** @brief multiple the function used to compute an array with n channels + mulipied by ccm. + @param xyz the input array, type of Mat. + @param ccm the ccm matrix to make color correction. + */ +Mat multiple(const Mat& xyz, const Mat& ccm); + +/** @brief multiple the function used to get the mask of saturated colors, + colors between low and up will be choosed. + @param src the input array, type of Mat. + @param low the threshold to choose saturated colors + @param up the threshold to choose saturated colors +*/ +Mat saturate(Mat& src, const double& low, const double& up); + +/** @brief rgb2gray it is an approximation grayscale function for relative RGB + color space + @param rgb the input array,type of Mat. + */ +Mat rgb2gray(const Mat& rgb); + +/** @brief function for elementWise operation + @param src the input array, type of Mat + @param lambda a for operation + */ +template +Mat elementWise(const Mat& src, F&& lambda) +{ + Mat dst = src.clone(); + const int channel = src.channels(); + switch (channel) + { + case 1: + { + + MatIterator_ it, end; + for (it = dst.begin(), end = dst.end(); it != end; ++it) + { + (*it) = lambda((*it)); + } + break; + } + case 3: + { + MatIterator_ it, end; + for (it = dst.begin(), end = dst.end(); it != end; ++it) + { + for (int j = 0; j < 3; j++) + { + (*it)[j] = lambda((*it)[j]); + } + } + break; + } + default: + CV_Error(Error::StsBadArg, "Wrong channel!" ); + break; + } + return dst; +} + +/** @brief function for channel operation + @param src the input array, type of Mat + @param lambda the function for operation +*/ +template +Mat channelWise(const Mat& src, F&& lambda) +{ + Mat dst = src.clone(); + MatIterator_ it, end; + for (it = dst.begin(), end = dst.end(); it != end; ++it) + { + *it = lambda(*it); + } + return dst; +} + +/** @brief function for distance operation. + @param src the input array, type of Mat. + @param ref another input array, type of Mat. + @param lambda the computing method for distance . + */ +template +Mat distanceWise(Mat& src, Mat& ref, F&& lambda) +{ + Mat dst = Mat(src.size(), CV_64FC1); + MatIterator_ it_src = src.begin(), end_src = src.end(), + it_ref = ref.begin(); + MatIterator_ it_dst = dst.begin(); + for (; it_src != end_src; ++it_src, ++it_ref, ++it_dst) + { + *it_dst = lambda(*it_src, *it_ref); + } + return dst; +} + +Mat multiple(const Mat& xyz, const Mat& ccm); + +} +} // namespace cv::ccm + +#endif \ No newline at end of file diff --git a/modules/mcc/test/test_ccm.cpp b/modules/mcc/test/test_ccm.cpp new file mode 100644 index 00000000000..56ac51db410 --- /dev/null +++ b/modules/mcc/test/test_ccm.cpp @@ -0,0 +1,166 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "test_precomp.hpp" + +namespace opencv_test +{ +namespace +{ + +Mat s = (Mat_(24, 1) << + Vec3d(214.11, 98.67, 37.97), + Vec3d(231.94, 153.1, 85.27), + Vec3d(204.08, 143.71, 78.46), + Vec3d(190.58, 122.99, 30.84), + Vec3d(230.93, 148.46, 100.84), + Vec3d(228.64, 206.97, 97.5), + Vec3d(229.09, 137.07, 55.29), + Vec3d(189.21, 111.22, 92.66), + Vec3d(223.5, 96.42, 75.45), + Vec3d(201.82, 69.71, 50.9), + Vec3d(240.52, 196.47, 59.3), + Vec3d(235.73, 172.13, 54.), + Vec3d(131.6, 75.04, 68.86), + Vec3d(189.04, 170.43, 42.05), + Vec3d(222.23, 74., 71.95), + Vec3d(241.01, 199.1, 61.15), + Vec3d(224.99, 101.4, 100.24), + Vec3d(174.58, 152.63, 91.52), + Vec3d(248.06, 227.69, 140.5), + Vec3d(241.15, 201.38, 115.58), + Vec3d(236.49, 175.87, 88.86), + Vec3d(212.19, 133.49, 54.79), + Vec3d(181.17, 102.94, 36.18), + Vec3d(115.1, 53.77, 15.23)); + +TEST(CV_ccmRunColorCorrection, test_model) +{ + + ColorCorrectionModel model(s / 255, COLORCHECKER_Macbeth); + model.run(); + Mat src_rgbl = (Mat_(24, 1) << + Vec3d(0.68078957, 0.12382801, 0.01514889), + Vec3d(0.81177942, 0.32550452, 0.089818), + Vec3d(0.61259378, 0.2831933, 0.07478902), + Vec3d(0.52696493, 0.20105976, 0.00958657), + Vec3d(0.80402284, 0.30419523, 0.12989841), + Vec3d(0.78658646, 0.63184111, 0.12062068), + Vec3d(0.78999637, 0.25520249, 0.03462853), + Vec3d(0.51866697, 0.16114393, 0.1078387), + Vec3d(0.74820768, 0.11770076, 0.06862177), + Vec3d(0.59776825, 0.05765816, 0.02886627), + Vec3d(0.8793145, 0.56346033, 0.0403954), + Vec3d(0.84124847, 0.42120746, 0.03287592), + Vec3d(0.23333214, 0.06780408, 0.05612276), + Vec3d(0.5176423, 0.41210976, 0.01896255), + Vec3d(0.73888613, 0.06575388, 0.06181293), + Vec3d(0.88326036, 0.58018751, 0.04321991), + Vec3d(0.75922531, 0.13149072, 0.1282041), + Vec3d(0.4345097, 0.32331019, 0.10494139), + Vec3d(0.94110142, 0.77941419, 0.26946323), + Vec3d(0.88438952, 0.5949049 , 0.17536928), + Vec3d(0.84722687, 0.44160449, 0.09834799), + Vec3d(0.66743106, 0.24076803, 0.03394333), + Vec3d(0.47141286, 0.13592419, 0.01362205), + Vec3d(0.17377101, 0.03256864, 0.00203026)); + ASSERT_MAT_NEAR(src_rgbl, model.get_src_rgbl(), 1e-4); + + Mat dst_rgbl = (Mat_(24, 1) << + Vec3d(0.17303173, 0.08211037, 0.05672686), + Vec3d(0.56832031, 0.29269488, 0.21835529), + Vec3d(0.10365019, 0.19588357, 0.33140475), + Vec3d(0.10159676, 0.14892193, 0.05188294), + Vec3d(0.22159627, 0.21584476, 0.43461196), + Vec3d(0.10806379, 0.51437196, 0.41264213), + Vec3d(0.74736423, 0.20062878, 0.02807988), + Vec3d(0.05757947, 0.10516793, 0.40296109), + Vec3d(0.56676218, 0.08424805, 0.11969461), + Vec3d(0.11099515, 0.04230796, 0.14292554), + Vec3d(0.34546869, 0.50872001, 0.04944204), + Vec3d(0.79461323, 0.35942459, 0.02051968), + Vec3d(0.01710416, 0.05022043, 0.29220674), + Vec3d(0.05598012, 0.30021149, 0.06871162), + Vec3d(0.45585457, 0.03033727, 0.04085654), + Vec3d(0.85737614, 0.56757335, 0.0068503), + Vec3d(0.53348585, 0.08861148, 0.30750446), + Vec3d(-0.0374061, 0.24699498, 0.40041217), + Vec3d(0.91262695, 0.91493909, 0.89367049), + Vec3d(0.57981916, 0.59200418, 0.59328881), + Vec3d(0.35490581, 0.36544831, 0.36755375), + Vec3d(0.19007357, 0.19186587, 0.19308397), + Vec3d(0.08529188, 0.08887994, 0.09257601), + Vec3d(0.0303193, 0.03113818, 0.03274845)); + ASSERT_MAT_NEAR(dst_rgbl, model.get_dst_rgbl(), 1e-4); + + Mat mask = Mat::ones(24, 1, CV_8U); + ASSERT_MAT_NEAR(model.getMask(), mask, 0.0); + + + Mat ccm = (Mat_(3, 3) << + 0.37408717, 0.02066172, 0.05796725, + 0.12684056, 0.77364991, -0.01566532, + -0.27464866, 0.00652140, 2.74593262); + ASSERT_MAT_NEAR(model.getCCM(), ccm, 1e-4); +} +TEST(CV_ccmRunColorCorrection, test_masks_weights_1) +{ + Mat weights_list_ = (Mat_(24, 1) << + 1.1, 0, 0, 1.2, 0, 0, + 1.3, 0, 0, 1.4, 0, 0, + 0.5, 0, 0, 0.6, 0, 0, + 0.7, 0, 0, 0.8, 0, 0); + ColorCorrectionModel model1(s / 255,COLORCHECKER_Macbeth); + model1.setColorSpace(COLOR_SPACE_sRGB); + model1.setCCM_TYPE(CCM_3x3); + model1.setDistance(DISTANCE_CIE2000); + model1.setLinear(LINEARIZATION_GAMMA); + model1.setLinearGamma(2.2); + model1.setLinearDegree(3); + model1.setSaturatedThreshold(0, 0.98); + model1.setWeightsList(weights_list_); + model1.setWeightCoeff(1.5); + model1.run(); + Mat weights = (Mat_(8, 1) << + 1.15789474, 1.26315789, 1.36842105, 1.47368421, + 0.52631579, 0.63157895, 0.73684211, 0.84210526); + ASSERT_MAT_NEAR(model1.getWeights(), weights, 1e-4); + + Mat mask = (Mat_(24, 1) << + true, false, false, true, false, false, + true, false, false, true, false, false, + true, false, false, true, false, false, + true, false, false, true, false, false); + ASSERT_MAT_NEAR(model1.getMask(), mask, 0.0); +} + +TEST(CV_ccmRunColorCorrection, test_masks_weights_2) +{ + ColorCorrectionModel model2(s / 255, COLORCHECKER_Macbeth); + model2.setCCM_TYPE(CCM_3x3); + model2.setDistance(DISTANCE_CIE2000); + model2.setLinear(LINEARIZATION_GAMMA); + model2.setLinearGamma(2.2); + model2.setLinearDegree(3); + model2.setSaturatedThreshold(0.05, 0.93); + model2.setWeightsList(Mat()); + model2.setWeightCoeff(1.5); + model2.run(); + Mat weights = (Mat_(20, 1) << + 0.65554256, 1.49454705, 1.00499244, 0.79735434, 1.16327759, + 1.68623868, 1.37973155, 0.73213388, 1.0169629, 0.47430246, + 1.70312161, 0.45414218, 1.15910007, 0.7540434, 1.05049802, + 1.04551645, 1.54082353, 1.02453421, 0.6015915, 0.26154558); + ASSERT_MAT_NEAR(model2.getWeights(), weights, 1e-4); + + Mat mask = (Mat_(24, 1) << + true, true, true, true, true, true, + true, true, true, true, false, true, + true, true, true, false, true, true, + false, false, true, true, true, true); + ASSERT_MAT_NEAR(model2.getMask(), mask, 0.0); +} + +} // namespace +} // namespace opencv_test diff --git a/modules/mcc/test/test_precomp.hpp b/modules/mcc/test/test_precomp.hpp index 1f399441353..c4d81a348c5 100644 --- a/modules/mcc/test/test_precomp.hpp +++ b/modules/mcc/test/test_precomp.hpp @@ -6,11 +6,14 @@ #define __OPENCV_TEST_PRECOMP_HPP__ #include "opencv2/ts.hpp" +#include "opencv2/ts/cuda_test.hpp" #include "opencv2/mcc.hpp" +#include "opencv2/mcc/ccm.hpp" namespace opencv_test { using namespace cv::mcc; +using namespace cv::ccm; } #endif diff --git a/modules/mcc/tutorials/basic_ccm/color_correction_model.markdown b/modules/mcc/tutorials/basic_ccm/color_correction_model.markdown new file mode 100644 index 00000000000..76b98cd3f4b --- /dev/null +++ b/modules/mcc/tutorials/basic_ccm/color_correction_model.markdown @@ -0,0 +1,122 @@ +Color Correction Model{#tutorial_ccm_color_correction_model} +=========================== + +In this tutorial you will learn how to use the 'Color Correction Model' to do a color correction in a image. + +Reference +---- + +See details of ColorCorrection Algorithm at https://github.com/riskiest/color_calibration/tree/v4/doc/pdf/English/Algorithm + +Building +---- + +When building OpenCV, run the following command to build all the contrib modules: + +```make +cmake -D OPENCV_EXTRA_MODULES_PATH=/modules/ +``` + +Or only build the mcc module: + +```make +cmake -D OPENCV_EXTRA_MODULES_PATH=/modules/mcc +``` + +Or make sure you check the mcc module in the GUI version of CMake: cmake-gui. + +Source Code of the sample +----------- + +The sample has two parts of code, the first is the color checker detector model, see details at @ref tutorial_mcc_basic_chart_detection, the second part is to make collor calibration. + +``` +Here are the parameters for ColorCorrectionModel + src : + detected colors of ColorChecker patches; + NOTICE: the color type is RGB not BGR, and the color values are in [0, 1]; + constcolor : + the Built-in color card; + Supported list: + Macbeth: Macbeth ColorChecker ; + Vinyl: DKK ColorChecker ; + DigitalSG: DigitalSG ColorChecker with 140 squares; + Mat colors : + the reference color values + and corresponding color space + NOTICE: the color values are in [0, 1] + ref_cs : + the corresponding color space + If the color type is some RGB, the format is RGB not BGR; + Supported Color Space: + Supported list of RGB color spaces: + COLOR_SPACE_sRGB; + COLOR_SPACE_AdobeRGB; + COLOR_SPACE_WideGamutRGB; + COLOR_SPACE_ProPhotoRGB; + COLOR_SPACE_DCI_P3_RGB; + COLOR_SPACE_AppleRGB; + COLOR_SPACE_REC_709_RGB; + COLOR_SPACE_REC_2020_RGB; + Supported list of linear RGB color spaces: + COLOR_SPACE_sRGBL; + COLOR_SPACE_AdobeRGBL; + COLOR_SPACE_WideGamutRGBL; + COLOR_SPACE_ProPhotoRGBL; + COLOR_SPACE_DCI_P3_RGBL; + COLOR_SPACE_AppleRGBL; + COLOR_SPACE_REC_709_RGBL; + COLOR_SPACE_REC_2020_RGBL; + Supported list of non-RGB color spaces: + COLOR_SPACE_Lab_D50_2; + COLOR_SPACE_Lab_D65_2; + COLOR_SPACE_XYZ_D50_2; + COLOR_SPACE_XYZ_D65_2; + COLOR_SPACE_XYZ_D65_10; + COLOR_SPACE_XYZ_D50_10; + COLOR_SPACE_XYZ_A_2; + COLOR_SPACE_XYZ_A_10; + COLOR_SPACE_XYZ_D55_2; + COLOR_SPACE_XYZ_D55_10; + COLOR_SPACE_XYZ_D75_2; + COLOR_SPACE_XYZ_D75_10; + COLOR_SPACE_XYZ_E_2; + COLOR_SPACE_XYZ_E_10; + COLOR_SPACE_Lab_D65_10; + COLOR_SPACE_Lab_D50_10; + COLOR_SPACE_Lab_A_2; + COLOR_SPACE_Lab_A_10; + COLOR_SPACE_Lab_D55_2; + COLOR_SPACE_Lab_D55_10; + COLOR_SPACE_Lab_D75_2; + COLOR_SPACE_Lab_D75_10; + COLOR_SPACE_Lab_E_2; + COLOR_SPACE_Lab_E_10; +``` + + +## Code + +@snippet samples/color_correction_model.cpp tutorial + +## Explanation + +The first part is to detect the ColorChecker position. +@snippet samples/color_correction_model.cpp get_color_checker +@snippet samples/color_correction_model.cpp get_messages_of_image +Preparation for ColorChecker detection to get messages for the image. + +@snippet samples/color_correction_model.cpp create +The CCheckerDetectorobject is created and uses getListColorChecker function to get ColorChecker message. + +@snippet samples/color_correction_model.cpp get_ccm_Matrix +For every ColorChecker, we can compute a ccm matrix for color correction. Model1 is an object of ColorCorrectionModel class. The parameters should be changed to get the best effect of color correction. See other parameters' detail at the Parameters. + +@snippet samples/color_correction_model.cpp reference_color_values +If you use a customized ColorChecker, you can use your own reference color values and corresponding color space as shown above. + +@snippet samples/color_correction_model.cpp make_color_correction +The member function infer_image is to make correction correction using ccm matrix. + +@snippet samples/color_correction_model.cpp Save_calibrated_image +Save the calibrated image. diff --git a/modules/mcc/tutorials/table_of_content_ccm.markdown b/modules/mcc/tutorials/table_of_content_ccm.markdown new file mode 100644 index 00000000000..178f309b993 --- /dev/null +++ b/modules/mcc/tutorials/table_of_content_ccm.markdown @@ -0,0 +1,8 @@ +Color Correction Model {#tutorial_table_of_content_ccm} +=========================== + +- @subpage tutorial_ccm_color_correction_model + + *Author:* riskiest, shanchenqi, JinhengZhang + + How to do color correction, using Color Correction Model. \ No newline at end of file From f10d54be8558ad1e07b945750b8488ae80432631 Mon Sep 17 00:00:00 2001 From: Kong Liangqian Date: Tue, 24 Nov 2020 19:39:27 +0800 Subject: [PATCH 27/29] fix tutorials foc:Do not display pictures by default --- .../viz/tutorials/transformations/transformations.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/viz/tutorials/transformations/transformations.markdown b/modules/viz/tutorials/transformations/transformations.markdown index 44d2cd47d91..b5c73db8968 100644 --- a/modules/viz/tutorials/transformations/transformations.markdown +++ b/modules/viz/tutorials/transformations/transformations.markdown @@ -84,8 +84,8 @@ Results -# Here is the result from the camera point of view. - ![](images/camera_view_point.png) +![](images/camera_view_point.png) -# Here is the result from global point of view. - ![](images/global_view_point.png) +![](images/global_view_point.png) \ No newline at end of file From f63965759d1828b68db85d5ecb97e132d0010f1a Mon Sep 17 00:00:00 2001 From: Akash Sharma Date: Wed, 25 Nov 2020 04:28:19 -0500 Subject: [PATCH 28/29] Merge pull request #2751 from akashsharma02:master Add depth_factor argument in rescaleDepth and typedef dynafu::Params for backwards compatibility * Add depth factor argument (default = 1000.0) to rescaleDepth to potentially support TUM type datasets * typedef dynafu::Params as kinfu::Params for compatibility --- modules/rgbd/include/opencv2/rgbd/depth.hpp | 5 +++-- modules/rgbd/include/opencv2/rgbd/dynafu.hpp | 6 ++++++ modules/rgbd/src/utils.cpp | 6 +++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/rgbd/include/opencv2/rgbd/depth.hpp b/modules/rgbd/include/opencv2/rgbd/depth.hpp index 0fcd5ce7d27..a50af0e1c6d 100755 --- a/modules/rgbd/include/opencv2/rgbd/depth.hpp +++ b/modules/rgbd/include/opencv2/rgbd/depth.hpp @@ -312,16 +312,17 @@ namespace rgbd depthTo3d(InputArray depth, InputArray K, OutputArray points3d, InputArray mask = noArray()); /** If the input image is of type CV_16UC1 (like the Kinect one), the image is converted to floats, divided - * by 1000 to get a depth in meters, and the values 0 are converted to std::numeric_limits::quiet_NaN() + * by depth_factor to get a depth in meters, and the values 0 are converted to std::numeric_limits::quiet_NaN() * Otherwise, the image is simply converted to floats * @param in the depth image (if given as short int CV_U, it is assumed to be the depth in millimeters * (as done with the Microsoft Kinect), it is assumed in meters) * @param depth the desired output depth (floats or double) * @param out The rescaled float depth image + * @param depth_factor (optional) factor by which depth is converted to distance (by default = 1000.0 for Kinect sensor) */ CV_EXPORTS_W void - rescaleDepth(InputArray in, int depth, OutputArray out); + rescaleDepth(InputArray in, int depth, OutputArray out, double depth_factor = 1000.0); /** Object that can compute planes in an image */ diff --git a/modules/rgbd/include/opencv2/rgbd/dynafu.hpp b/modules/rgbd/include/opencv2/rgbd/dynafu.hpp index fae69c48eef..55fe36d7e12 100644 --- a/modules/rgbd/include/opencv2/rgbd/dynafu.hpp +++ b/modules/rgbd/include/opencv2/rgbd/dynafu.hpp @@ -13,6 +13,7 @@ #include "kinfu.hpp" namespace cv { + namespace dynafu { /** @brief DynamicFusion implementation @@ -37,6 +38,11 @@ namespace dynafu { That's why you need to set the OPENCV_ENABLE_NONFREE option in CMake to use DynamicFusion. */ + + +/** Backwards compatibility for old versions */ +using Params = kinfu::Params; + class CV_EXPORTS_W DynaFu { public: diff --git a/modules/rgbd/src/utils.cpp b/modules/rgbd/src/utils.cpp index 30c8aa153f2..b91a88d4df1 100644 --- a/modules/rgbd/src/utils.cpp +++ b/modules/rgbd/src/utils.cpp @@ -22,7 +22,7 @@ namespace rgbd * @param out_out The rescaled float depth image */ void - rescaleDepth(InputArray in_in, int depth, OutputArray out_out) + rescaleDepth(InputArray in_in, int depth, OutputArray out_out, double depth_factor) { cv::Mat in = in_in.getMat(); CV_Assert(in.type() == CV_64FC1 || in.type() == CV_32FC1 || in.type() == CV_16UC1 || in.type() == CV_16SC1); @@ -34,13 +34,13 @@ namespace rgbd cv::Mat out = out_out.getMat(); if (in_depth == CV_16U) { - in.convertTo(out, depth, 1 / 1000.0); //convert to float so that it is in meters + in.convertTo(out, depth, 1 / depth_factor); //convert to float so that it is in meters cv::Mat valid_mask = in == std::numeric_limits::min(); // Should we do std::numeric_limits::max() too ? out.setTo(std::numeric_limits::quiet_NaN(), valid_mask); //set a$ } if (in_depth == CV_16S) { - in.convertTo(out, depth, 1 / 1000.0); //convert to float so tha$ + in.convertTo(out, depth, 1 / depth_factor); //convert to float so tha$ cv::Mat valid_mask = (in == std::numeric_limits::min()) | (in == std::numeric_limits::max()); // Should we do std::numeric_limits::max() too ? out.setTo(std::numeric_limits::quiet_NaN(), valid_mask); //set a$ } From 89e856b3fa428e304a8389f9ca56a7e26af001b4 Mon Sep 17 00:00:00 2001 From: Alexander Alekhin Date: Thu, 26 Nov 2020 09:22:44 +0000 Subject: [PATCH 29/29] build: xcode 12 support --- modules/intensity_transform/src/bimef.cpp | 2 +- modules/rgbd/src/submap.hpp | 6 +++--- modules/sfm/src/libmv_light/CMakeLists.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/intensity_transform/src/bimef.cpp b/modules/intensity_transform/src/bimef.cpp index 58eac6002af..72555ed9773 100644 --- a/modules/intensity_transform/src/bimef.cpp +++ b/modules/intensity_transform/src/bimef.cpp @@ -234,7 +234,7 @@ static Mat solveLinearEquation(const Mat_& img, Mat_& W_h_, Mat_ tsmooth(const Mat_& src, float lambda=0.01f, float sigma=3.0f, float sharpness=0.001f) diff --git a/modules/rgbd/src/submap.hpp b/modules/rgbd/src/submap.hpp index 983ee610c75..65a2bed6229 100644 --- a/modules/rgbd/src/submap.hpp +++ b/modules/rgbd/src/submap.hpp @@ -499,7 +499,7 @@ PoseGraph SubmapManager::MapToPoseGraph() PoseGraph localPoseGraph; - for(const Ptr currSubmap : submapList) + for(const auto& currSubmap : submapList) { const typename SubmapT::Constraints& constraintList = currSubmap->constraints; for(const auto& currConstraintPair : constraintList) @@ -512,7 +512,7 @@ PoseGraph SubmapManager::MapToPoseGraph() } } - for(const Ptr currSubmap : submapList) + for(const auto& currSubmap : submapList) { PoseGraphNode currNode(currSubmap->id, currSubmap->pose); if(currSubmap->id == 0) @@ -530,7 +530,7 @@ PoseGraph SubmapManager::MapToPoseGraph() template void SubmapManager::PoseGraphToMap(const PoseGraph &updatedPoseGraph) { - for(const Ptr currSubmap : submapList) + for(const auto& currSubmap : submapList) { const PoseGraphNode& currNode = updatedPoseGraph.nodes.at(currSubmap->id); if(!currNode.isPoseFixed()) diff --git a/modules/sfm/src/libmv_light/CMakeLists.txt b/modules/sfm/src/libmv_light/CMakeLists.txt index aee38c68184..0a1549ab986 100644 --- a/modules/sfm/src/libmv_light/CMakeLists.txt +++ b/modules/sfm/src/libmv_light/CMakeLists.txt @@ -7,7 +7,7 @@ ocv_warnings_disable(CMAKE_CXX_FLAGS -Winconsistent-missing-override -Wsuggest-o if(CV_GCC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 8.0) ocv_warnings_disable(CMAKE_CXX_FLAGS -Wclass-memaccess) endif() -if(CV_GCC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0) +if((CV_GCC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0) OR CV_CLANG) ocv_warnings_disable(CMAKE_CXX_FLAGS -Wdeprecated-copy) endif()