From f77843f4a5092ba14877ee1a5e2589eaedab8c6f Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Tue, 8 Jun 2021 20:13:46 +0300 Subject: [PATCH] Move ROS Noetic (#327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * builder: Use 64-bit Raspberry Pi OS * travis: Use 64-bit builder * builder: Don't try to install Melodic packages on Noetic * clover: Use package version 3, update dependencies * travis: Enable Noetic build * standalone_install: Auto-select Python, ROS distro * builder: Use variable substitution for ROS_DISTRO * builder: Add Noetic package definitions * builder: Use variable substitution for validation * aruco_pose, clover: Allow compiling against OpenCV 3 and 4 * builder: Add proper Noetic repository * builder: Don't force Tornado version Assume rosbridge_suite depends on the right one. * builder: Install packages for Python 3 * builder/test: Use Python3 interpreter for ROS tests TODO (?): add tests for Python2? * builder: Use Python 3 syntax for Python 3 tests * builder: Install rpi_ws281x for Python3 * standalone_install: Use proper Python for pytest * builder: Install espeak for python3 * builder: Use proper path for roscore * builder: Install rosdep, etc. for python3 * builder: Run Clever/Clover test with Python3 * builder: Use Python3 for Clever compat layer * builder: Enable OpenCV 4.2 repository * builder: Force versions for ROS packages that use OpenCV Also, hold their versions so that they don't get updated for no reason. * aruco_pose/draw: Replace OpenCV projection code with a rewrite * builder: Don't try to install compressed_transport twice * clover: Fix importing urllib for Python3 * aruco_pose, clover: Expose Python scripts through CMake * clover/selfcheck: Be more python3-compatible This is basically commit a01d199890f06716a7149f1117326272a4025eef from buster-python3, not sure if it aged well. * roswww_static: Add python script installation * clover_blocks: Use Python3 syntax for exec * aruco_pose: Remove unused code * Melodic => Noetic in some docs * docs: add 0.22 migration article * docs: remove unneeded comment * docs: python 3 updates * docs: python 3 update in auto_setup article * docs: add ROS Noetic transition note * aruco.launch: add placement, length and map arguments * genmap.py: add -o argument for output file name * docs: use -o argument of genmap.py * simple_offboard: correctly check manual control timeout, separate it from kill switch check * blocks: force led_leds index to int * docs: update and fix 0.22 migration articles * blocks: fix set_leds with color-typed argument * aruco_gen: Open file in binary mode for Python3 compatibility * clover: Use proper variable in aruco.launch * led: change default number of leds to 72 * aruco_pose: Make sure there are no undefined symbols Also, compile in apriltag_quad_thresh.cpp - it contains some of the functions referenced in aruco.cpp, which would otherwise be undefined. * aruco_pose: Make vendored library compatible with older OpenCVs * aruco_pose, clover: Reduce the amount of OpenCV libs requested * aruco_pose, clover: Move subscriptions to the end of init * aruco_pose: Don't expose vendored library symbols * aruco_pose: Simplify dynamic parameter callback setting * builder: Build with debug symbols * clover: Attempt to respawn dying nodelets * Change Raspberry Pi OS to latest armhf, use packages.coex.tech as a source * Add CRYPTOGRAPHY_DONT_BUILD_RUST=1 * Fix Node.js installation * image: use older CMake (3.13.4-1) Fixing https://travis-ci.org/github/CopterExpress/clover/jobs/764367665#L6984 * image: update Raspberry Pi OS to 2021-03-04 * image: bring back moving ld.so.preload out of the way while building * Fix pthreads ld error * Try to fix pthreads ld error * Another attempt to fix pthreads ld error * Yet another attempt to fix pthreads ld error * Try to fix * Be verbose * Temporarily disable rc and camera_markers building * Fix standalone-install * Revert "Temporarily disable rc and camera_markers building" This reverts commit e119220e911551c3f2364caf1de02d32a52509ec. * Try to fix * Try to fix * Revert "image: use older CMake (3.13.4-1)" This reverts commit df28da0060a00c4225570672e692c4ae01107d71. * Revert "Revert "image: use older CMake (3.13.4-1)"" This reverts commit a28c774e8f1a07ba66b53104fc2f830e495930c4. * Verbosity * Debugging * More debugging * Display all CMake variables * Try to fix * Another try to fix * Revert "Another try to fix" This reverts commit 5a4c3a0da75be0f5bd0096e265039848cf9d847f. * Another try to fix * And another * And yet another * Continue... * Cleanup * Sources lists cleanup * More cleanup * Restore .git directory in clover repo * Fix building documentation * Fix documentation building in image * Trigger build to update ws281x package * Test * Disable unneeded hack * Disable hack * image: add cmake-modules package * www: add viewing clover.err file from web interface * Remove hacks * Show nodelet version * docs: add packages article * image: add image-view package for recording video from topics * Minor fix * CI: add Docker authentication on image build * CI: fix Bash syntax * CI: fix authentication in Docker * CI: move Melodic build and editorconfig-lint to GitHub Actions (#331) * Create main.yml * Update main.yml * Disable native Melodic build in Travis * Run editorconfig-lint in Actions * Let wget be less verbose * Test * Test ok * Disable editorconfig-lint in Travis * docs: add links to hardware sources * CI: move image building to GitHub actions (#335) * Start working on building image in GitHub actions * Trigger GitHub on push to any branch * Fix TRAVIS_TAG * Add compress image step * Disable image build in Travis * Add upload image step * Fix compress image * Fix * Fix * Minor fix * Trigger build on tag * Show images sizes not in human format * Upload only built image * Make prerelease * Upload assets on release not on tags * readme: change build badge to GitHub Actions * readme: add support chat badge * CI: move documentation building to GitHub Actions (#337) * CI: change docs target branch to actions * CI: change docs target branch to master * CI: use gh-pages target branch for docs * CI: split up to several workflows * CI: remove .travis.yml * CI: change apt to apt-get * CI: push documentation site to the main repo * builder: less verbosity * CI: add new key for apt Fixing https://github.com/CopterExpress/clover/runs/2700356960#step:3:74 * Add Noetic building to CI * Add test for QR recognition * Fix * Move QR recognition test to a separate file * Fix QR recognition code for Python 3 * Import SetLEDs, LEDStateArray, LEDState in tests * Add more imports to tests (from documentation) * Fix permissions * Fix standalone-install for Python 2 * Fix QR recognition test * Don’t use ROS for QR recognition test * docs: remove non-working example * Make v4l2 device file an argument in main_camera.launch * Wait for v4l2 device before launching the camera driver * Use exec in waitfile * Transfer main camera nodelet manager to main_camera.launch * Update cv_camera version to 0.5.1 * docs: minor fix * Revert cv_camera to 0.5.0 * Update Raspberry Pi OS to 2021-05-07 * docs: add link to the last ROS Melodic version. Co-authored-by: Alexey Rogachevskiy --- .github/workflows/build.yml | 7 + .markdownlint.json | 1 + README.md | 2 +- aruco_pose/CMakeLists.txt | 23 +- aruco_pose/src/aruco_detect.cpp | 5 +- aruco_pose/src/draw.cpp | 886 ++++-------------- aruco_pose/vendor/VendorOpenCV.cmake | 3 +- .../vendor/aruco/src/apriltag_quad_thresh.cpp | 14 +- builder/assets/clever/setup.py | 2 +- builder/assets/noetic-rosdep-clover.yaml | 18 + builder/assets/roscore.service | 2 +- builder/image-build.sh | 2 +- builder/image-ros.sh | 66 +- builder/image-software.sh | 19 +- builder/image-validate.sh | 8 +- builder/standalone-install.sh | 35 +- builder/test/qr.png | Bin 0 -> 1848 bytes builder/test/test_qr.py | 42 + builder/test/tests.py | 14 +- builder/test/tests.sh | 2 + builder/test/tests_clever.py | 2 +- clover/CMakeLists.txt | 16 +- clover/README.md | 2 +- clover/launch/aruco.launch | 19 +- clover/launch/clover.launch | 7 +- clover/launch/main_camera.launch | 10 +- clover/package.xml | 5 +- clover/src/selfcheck.py | 10 +- clover/test/basic.py | 9 +- clover_blocks/src/clover_blocks | 2 +- clover_blocks/www/python.js | 2 +- .../src/clover_simulation/__init__.py | 2 +- docs/assets/noetic.png | Bin 0 -> 49073 bytes docs/en/SUMMARY.md | 2 + docs/en/aruco_map.md | 15 +- docs/en/aruco_marker.md | 24 +- docs/en/auto_setup.md | 4 +- docs/en/camera.md | 14 +- docs/en/cli.md | 2 +- docs/en/contributing.md | 4 + docs/en/image.md | 2 + docs/en/laser.md | 2 +- docs/en/migrate20.md | 50 - docs/en/migrate22.md | 59 ++ docs/en/packages.md | 27 + docs/en/programming.md | 4 +- docs/en/ros.md | 2 +- docs/en/simple_offboard.md | 10 +- docs/en/snippets.md | 2 +- docs/en/sonar.md | 4 +- docs/ru/SUMMARY.md | 2 + docs/ru/aruco_map.md | 14 +- docs/ru/aruco_marker.md | 24 +- docs/ru/auto_setup.md | 4 +- docs/ru/camera.md | 14 +- docs/ru/cli.md | 2 +- docs/ru/contributing.md | 4 + docs/ru/image.md | 2 + docs/ru/laser.md | 2 +- docs/ru/migrate20.md | 50 - docs/ru/migrate22.md | 59 ++ docs/ru/packages.md | 27 + docs/ru/programming.md | 4 +- docs/ru/ros.md | 2 +- docs/ru/simple_offboard.md | 10 +- docs/ru/snippets.md | 2 +- docs/ru/sonar.md | 4 +- roswww_static/CMakeLists.txt | 4 + 68 files changed, 750 insertions(+), 944 deletions(-) create mode 100644 builder/assets/noetic-rosdep-clover.yaml create mode 100644 builder/test/qr.png create mode 100755 builder/test/test_qr.py create mode 100644 docs/assets/noetic.png create mode 100644 docs/en/migrate22.md create mode 100644 docs/en/packages.md create mode 100644 docs/ru/migrate22.md create mode 100644 docs/ru/packages.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0709e465..2d409811 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,3 +14,10 @@ jobs: - name: Native Melodic build run: | docker run --rm -v $(pwd):/root/catkin_ws/src/clover ros:melodic-ros-base /root/catkin_ws/src/clover/builder/standalone-install.sh + build-noetic: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Native Noetic build + run: | + docker run --rm -v $(pwd):/root/catkin_ws/src/clover ros:noetic-ros-base /root/catkin_ws/src/clover/builder/standalone-install.sh diff --git a/.markdownlint.json b/.markdownlint.json index 9c2ab472..6ca8b315 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -21,6 +21,7 @@ "ROS", "ROS Kinetic", "ROS Melodic", + "ROS Noetic", "OpenCV", "OpenVPN", "Gazebo", diff --git a/README.md b/README.md index 8ff3b1b5..65445daf 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Preconfigured image for Raspberry Pi with installed and configured software, rea Image features: * Raspbian Buster -* [ROS Melodic](http://wiki.ros.org/melodic) +* [ROS Noetic](http://wiki.ros.org/noetic) * Configured networking * OpenCV * [`mavros`](http://wiki.ros.org/mavros) diff --git a/aruco_pose/CMakeLists.txt b/aruco_pose/CMakeLists.txt index f0176424..e3ab2a93 100644 --- a/aruco_pose/CMakeLists.txt +++ b/aruco_pose/CMakeLists.txt @@ -22,13 +22,21 @@ find_package(catkin REQUIRED COMPONENTS dynamic_reconfigure ) -find_package(OpenCV 3 REQUIRED COMPONENTS core imgproc calib3d) +# Workaround for OpenCV 3/4 support +set(_opencv_version 4) +find_package(OpenCV ${_opencv_version} QUIET COMPONENTS core imgproc calib3d) +if (NOT OpenCV_FOUND) + message(STATUS "Did not find OpenCV 4, searching for OpenCV 3") + set(_opencv_version 3) +endif() + +find_package(OpenCV ${_opencv_version} REQUIRED COMPONENTS core imgproc calib3d) if ("${OpenCV_VERSION_MINOR}" LESS "9") message(STATUS "OpenCV version too low, using vendored ArUco package") include(vendor/VendorOpenCV.cmake) else() message(STATUS "Using system OpenCV ArUco package") - find_package(OpenCV 3 REQUIRED COMPONENTS aruco) + find_package(OpenCV ${_opencv_version} REQUIRED COMPONENTS aruco) endif() message(STATUS "OpenCV include dirs: ${OpenCV_INCLUDE_DIRS}") message(STATUS "OpenCV libraries: ${OpenCV_LIBRARIES}") @@ -172,6 +180,13 @@ target_link_libraries(aruco_pose ${OpenCV_LIBRARIES} ) +# Prevent aruco_pose from having undefined symbols +set_property(TARGET aruco_pose + APPEND + PROPERTY LINK_FLAGS + -Wl,--no-undefined +) + ############# ## Install ## ############# @@ -207,6 +222,10 @@ target_link_libraries(aruco_pose # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} # ) +catkin_install_python(PROGRAMS src/genmap.py + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + ############# ## Testing ## ############# diff --git a/aruco_pose/src/aruco_detect.cpp b/aruco_pose/src/aruco_detect.cpp index f1658ac7..ae945876 100644 --- a/aruco_pose/src/aruco_detect.cpp +++ b/aruco_pose/src/aruco_detect.cpp @@ -112,10 +112,7 @@ public: image_transport::ImageTransport it_priv(nh_priv_); dyn_srv_ = std::make_shared>(nh_priv_); - dynamic_reconfigure::Server::CallbackType cb; - - cb = std::bind(&ArucoDetect::paramCallback, this, std::placeholders::_1, std::placeholders::_2); - dyn_srv_->setCallback(cb); + dyn_srv_->setCallback(std::bind(&ArucoDetect::paramCallback, this, std::placeholders::_1, std::placeholders::_2)); debug_pub_ = it_priv.advertise("debug", 1); markers_pub_ = nh_priv_.advertise("markers", 1); diff --git a/aruco_pose/src/draw.cpp b/aruco_pose/src/draw.cpp index d9cd0e7d..d8e7b021 100644 --- a/aruco_pose/src/draw.cpp +++ b/aruco_pose/src/draw.cpp @@ -3,26 +3,11 @@ #include "draw.h" #include +#include using namespace cv; using namespace cv::aruco; -static void _cvProjectPoints2( const CvMat* object_points, const CvMat* rotation_vector, - const CvMat* translation_vector, const CvMat* camera_matrix, - const CvMat* distortion_coeffs, CvMat* image_points, - CvMat* dpdrot CV_DEFAULT(NULL), CvMat* dpdt CV_DEFAULT(NULL), - CvMat* dpdf CV_DEFAULT(NULL), CvMat* dpdc CV_DEFAULT(NULL), - CvMat* dpddist CV_DEFAULT(NULL), - double aspect_ratio CV_DEFAULT(0)); - -static void _projectPoints( InputArray objectPoints, - InputArray rvec, InputArray tvec, - InputArray cameraMatrix, InputArray distCoeffs, - OutputArray imagePoints, - OutputArray jacobian = noArray(), - double aspectRatio = 0 ); - - void _drawPlanarBoard(Board *_board, Size outSize, OutputArray _img, int marginSize, int borderBits, bool drawAxis) { @@ -142,35 +127,194 @@ void _drawPlanarBoard(Board *_board, Size outSize, OutputArray _img, int marginS } } -/* Draw a (potentially partially visible) line. */ -static void linePartial(InputOutputArray image, Point3f p1, Point3f p2, const Scalar& color, - int thickness = 1, int lineType = LINE_8, int shift = 0) +/** + * @brief Convert point coordinates from world space to camera space. + * + * @param points A vector of points in world space. + * @param rvec Rotation matrix or Rodrigues rotation vector. + * @param tvec Translation vector from world to camera space. + * + * @return A vector of points in camera space. + */ +template +static std::vector worldToCamera(const std::vector& points, + const cv::Mat& rvec, const cv::Mat& tvec) { - // If both points are behind the screen, don't draw anything - if (p1.z <= 0 && p2.z <= 0) { - return; + // We operate with CV_64F matrices internally to avoid precision loss + cv::Mat rvec_64f; + cv::Mat tvec_64f; + rvec.convertTo(rvec_64f, CV_64F); + tvec.convertTo(tvec_64f, CV_64F); + + // Convert Rodrigues vector to rotation matrix + cv::Mat rmat; + if ((rvec_64f.cols == 3 && rvec_64f.rows == 1) || + (rvec_64f.cols == 1 && rvec_64f.rows == 3)) + { + Rodrigues(rvec_64f, rmat); } - Point2f p1p{p1.x, p1.y}; - Point2f p2p{p2.x, p2.y}; - // If points are on the different sides of the plane, compute intersection point - if (p1.z * p2.z < 0) { - // Compute intersection point with the screen - // We denote alpha as such: - // xi = (1 - alpha) * x1 + alpha * x2 - // yi = (1 - alpha) * y1 + alpha * y2 - // zi = (1 - alpha) * z1 + alpha * z2 = 0 - // Thus, alpha can be expressed as - // alpha = z1 / (z1 - z2) - float alpha = p1.z / (p1.z - p2.z); - Point2f pi{(1 - alpha) * p1.x + alpha * p2.x, (1 - alpha) * p1.y + alpha * p2.y}; - // Now, if z1 is negative, we draw the line from (xi, yi) to (x2, y2), else we draw from (x1, y1) to (xi, yi) - if (p1.z < 0) { - p1p = pi; - } else { - p2p = pi; - } + else + { + rmat = rvec_64f.clone(); } - line(image, p1p, p2p, color, thickness, lineType, shift); + // Make sure tvec has a size of (3, 1) + if (tvec_64f.rows == 1) + { + tvec_64f = tvec_64f.t(); + } + std::vector result; + result.reserve(points.size()); + for(const auto& point : points) + { + // Calculate point coordinates in camera frame + // static_casts are here to silence potential narrowing conversion warnings + CvPointType camPoint{ + static_cast(point.x * rmat.at(0,0) + point.y * rmat.at(0,1) + point.z * rmat.at(0,2) + tvec_64f.at(0)), + static_cast(point.x * rmat.at(1,0) + point.y * rmat.at(1,1) + point.z * rmat.at(1,2) + tvec_64f.at(1)), + static_cast(point.x * rmat.at(2,0) + point.y * rmat.at(2,1) + point.z * rmat.at(2,2) + tvec_64f.at(2)) + }; + result.push_back(camPoint); + } + return result; +} + +/** + * @brief Project points from camera space to screen space, applying distortion in the process. + * + * @param points A vector of points in camera space. + * @param cameraMatrix OpenCV intrinsic camera matrix. + * @param distCoeffs OpenCV distortion model coefficients. + * + * @return A vector of points in screen space. + */ +template +static std::vector cameraToScreen(const std::vector& points, + const cv::Mat& cameraMatrix, + const cv::Mat& distCoeffs) +{ + // We operate with CV_64F matrices internally to avoid precision loss + cv::Mat cm_64f; // camera matrix, CV_64F + cv::Mat dc_64f; // distortion coefficients, CV_64F + cameraMatrix.convertTo(cm_64f, CV_64F); + distCoeffs.convertTo(dc_64f, CV_64F); + + // Make sure distortion vector has a size of (N, 1) + if (dc_64f.rows == 1) + { + dc_64f = dc_64f.t(); + } + + // We will always use 12 distortion coefficients, + // and we can safely pad missing ones with zeroes + dc_64f.resize(12, 0.0); + + std::vector result; + result.reserve(points.size()); + + for(const auto& point : points) + { + // Apply perspective projection, preserving initial Z coordinate + // Always use double-precision + cv::Point3d camPoint{ + point.x / point.z, + point.y / point.z, + point.z + }; + + // Apply distortion + // Note that we do not consider tilted sensor distortion + // r^2 - distance from the image center squared + double r2 = camPoint.x * camPoint.x + camPoint.y * camPoint.y; + // r^4 - same, but to the 4th power + double r4 = r2 * r2; + // r^6 - same, but to the 6th power + double r6 = r4 * r2; + // tg1 - first tangential shift factor (2 * x * y) + double tg1 = 2 * camPoint.x * camPoint.y; + // tg2 - second tangential shift factor (r^2 + 2 * x^2) + double tg2 = r2 + 2 * camPoint.x * camPoint.x; + // tg3 - third tangential shift factor (r^2 + 2 * y^2) + double tg3 = r2 + 2 * camPoint.y * camPoint.y; + // polynomial distortion factor (numerator) + double pndist = 1 + dc_64f.at(0) * r2 + dc_64f.at(1) * r4 + dc_64f.at(4) * r6; + // polynomial distortion factror (denominator) + double pddist = 1.0 / (1 + dc_64f.at(5) * r2 + dc_64f.at(6) * r4 + dc_64f.at(7) * r6); + // Distorted point coordinates (always double-precision) + cv::Point3d distortedPoint{ + camPoint.x * pndist * pddist + dc_64f.at(2) * tg1 + dc_64f.at(3) * tg2 + dc_64f.at(8) * r2 + dc_64f.at(9) * r4, + camPoint.y * pndist * pddist + dc_64f.at(2) * tg3 + dc_64f.at(3) * tg1 + dc_64f.at(10) * r2 + dc_64f.at(11) * r4, + camPoint.z + }; + + // Convert to screen space + // We use static_cast here to silence potential warnings about narrowing conversions + // (we expect that to be the case) + CvPointType screenPoint{ + static_cast(distortedPoint.x * cm_64f.at(0, 0) + cm_64f.at(0, 2)), + static_cast(distortedPoint.y * cm_64f.at(1, 1) + cm_64f.at(1, 2)), + static_cast(distortedPoint.z) + }; + + result.push_back(screenPoint); + } + return result; +} + +/** + * @brief Clip a line against a clip plane. + * + * This function "clips" a line (described by two points in *camera space*) + * against a clip plane that is `clipPlaneDistance` meters away from the + * camera focal point. If both points are further away from the focal point + * than `clipPlaneDistance`, they will be returned unmodified. If one of the + * points is behind the clipping plane, a point *on* the clipping plane will + * be computed and returned as one of the points. + * + * If none of the points are visible, an empty vector will be returned. + * + * @param p1 First point on the line, in camera space. + * @param p2 Second point on the line, in camera space. + * @param clipPlaneDistance Distance from the focal point to the clipping plane. + * @return A vector of zero or two points on the clipped line, in camera space. + */ +static std::vector lineClip(Point3f p1, Point3f p2, float clipPlaneDistance = 0.1f) +{ + // We don't need to compute an intersection if both points are + // behind us + if (p1.z < clipPlaneDistance && p2.z < clipPlaneDistance) + { + return {}; + } + // We don't need to compute an intersection if both points are + // in front of us + if (p1.z > clipPlaneDistance && p2.z > clipPlaneDistance) + { + return {p1, p2}; + } + // We don't really want to compute an intersection if both Z coordinates + // are sufficiently close to each other + if (std::abs(p1.z - p2.z) < 0.0001) // The number here is chosen arbitrarily + { + return {p1, p2}; + } + // We compute the intersection as such: + // zi = (1 - alpha) * p1.z + alpha * p2.z = clipPlaneDistance + // alpha = (p1.z - clipPlaneDistance) / (p1.z - p2.z) + double alpha = (p1.z - clipPlaneDistance) / (p1.z - p2.z); + Point3f clipPlanePoint{ + static_cast((1 - alpha) * p1.x + alpha * p2.x), + static_cast((1 - alpha) * p1.y + alpha * p2.y), + clipPlaneDistance + }; + if (p1.z < clipPlaneDistance) + { + return {clipPlanePoint, p2}; + } + else + { + return {p1, clipPlanePoint}; + } + // Unreachable? } void _drawAxis(InputOutputArray _image, InputArray _cameraMatrix, InputArray _distCoeffs, @@ -186,647 +330,23 @@ void _drawAxis(InputOutputArray _image, InputArray _cameraMatrix, InputArray _di axisPoints.push_back(Point3f(length, 0, 0)); axisPoints.push_back(Point3f(0, length, 0)); axisPoints.push_back(Point3f(0, 0, length)); - std::vector imagePointsZ; - _projectPoints(axisPoints, _rvec, _tvec, _cameraMatrix, _distCoeffs, imagePointsZ); - - // draw axis lines - linePartial(_image, imagePointsZ[0], imagePointsZ[1], Scalar(0, 0, 255), 3); - linePartial(_image, imagePointsZ[0], imagePointsZ[2], Scalar(0, 255, 0), 3); - linePartial(_image, imagePointsZ[0], imagePointsZ[3], Scalar(255, 0, 0), 3); -} - -static CvMat _cvMat(const cv::Mat& m) -{ - CvMat self; - CV_DbgAssert(m.dims <= 2); - self = cvMat(m.rows, m.dims == 1 ? 1 : m.cols, m.type(), m.data); - self.step = (int)m.step[0]; - self.type = (self.type & ~cv::Mat::CONTINUOUS_FLAG) | (m.flags & cv::Mat::CONTINUOUS_FLAG); - return self; -} - -static void _projectPoints( InputArray _opoints, - InputArray _rvec, - InputArray _tvec, - InputArray _cameraMatrix, - InputArray _distCoeffs, - OutputArray _ipoints, - OutputArray _jacobian, - double aspectRatio ) -{ - Mat opoints = _opoints.getMat(); - int npoints = opoints.checkVector(3), depth = opoints.depth(); - CV_Assert(npoints >= 0 && (depth == CV_32F || depth == CV_64F)); - - CvMat dpdrot, dpdt, dpdf, dpdc, dpddist; - CvMat *pdpdrot = 0, *pdpdt = 0, *pdpdf = 0, *pdpdc = 0, *pdpddist = 0; - - CV_Assert(_ipoints.needed()); - - _ipoints.create(npoints, 1, CV_MAKETYPE(depth, 3), -1, true); - Mat imagePoints = _ipoints.getMat(); - CvMat c_imagePoints = _cvMat(imagePoints); - CvMat c_objectPoints = _cvMat(opoints); - Mat cameraMatrix = _cameraMatrix.getMat(); - - Mat rvec = _rvec.getMat(), tvec = _tvec.getMat(); - CvMat c_cameraMatrix = _cvMat(cameraMatrix); - CvMat c_rvec = _cvMat(rvec), c_tvec = _cvMat(tvec); - - double dc0buf[5] = {0}; - Mat dc0(5, 1, CV_64F, dc0buf); - Mat distCoeffs = _distCoeffs.getMat(); - if (distCoeffs.empty()) - distCoeffs = dc0; - CvMat c_distCoeffs = _cvMat(distCoeffs); - int ndistCoeffs = distCoeffs.rows + distCoeffs.cols - 1; - - Mat jacobian; - if (_jacobian.needed()) + auto camAxisPoints = worldToCamera(axisPoints, _rvec.getMat(), _tvec.getMat()); + auto axisX = cameraToScreen(lineClip(camAxisPoints[0], camAxisPoints[1]), _cameraMatrix.getMat(), _distCoeffs.getMat()); + auto axisY = cameraToScreen(lineClip(camAxisPoints[0], camAxisPoints[2]), _cameraMatrix.getMat(), _distCoeffs.getMat()); + auto axisZ = cameraToScreen(lineClip(camAxisPoints[0], camAxisPoints[3]), _cameraMatrix.getMat(), _distCoeffs.getMat()); + if (axisX.size() > 0) { - _jacobian.create(npoints * 2, 3 + 3 + 2 + 2 + ndistCoeffs, CV_64F); - jacobian = _jacobian.getMat(); - pdpdrot = &(dpdrot = _cvMat(jacobian.colRange(0, 3))); - pdpdt = &(dpdt = _cvMat(jacobian.colRange(3, 6))); - pdpdf = &(dpdf = _cvMat(jacobian.colRange(6, 8))); - pdpdc = &(dpdc = _cvMat(jacobian.colRange(8, 10))); - pdpddist = &(dpddist = _cvMat(jacobian.colRange(10, 10 + ndistCoeffs))); + line(_image, Point2f{axisX[0].x, axisX[0].y}, Point2f{axisX[1].x, axisX[1].y}, + Scalar(0, 0, 255), 3); + } + if (axisY.size() > 0) + { + line(_image, Point2f{axisY[0].x, axisY[0].y}, Point2f{axisY[1].x, axisY[1].y}, + Scalar(0, 255, 0), 3); + } + if (axisZ.size() > 0) + { + line(_image, Point2f{axisZ[0].x, axisZ[0].y}, Point2f{axisZ[1].x, axisZ[1].y}, + Scalar(255, 0, 0), 3); } - - _cvProjectPoints2(&c_objectPoints, &c_rvec, &c_tvec, &c_cameraMatrix, &c_distCoeffs, - &c_imagePoints, pdpdrot, pdpdt, pdpdf, pdpdc, pdpddist, aspectRatio); -} - -namespace _detail -{ - template - void computeTiltProjectionMatrix(FLOAT tauX, - FLOAT tauY, - Matx* matTilt = 0, - Matx* dMatTiltdTauX = 0, - Matx* dMatTiltdTauY = 0, - Matx* invMatTilt = 0) - { - FLOAT cTauX = cos(tauX); - FLOAT sTauX = sin(tauX); - FLOAT cTauY = cos(tauY); - FLOAT sTauY = sin(tauY); - Matx matRotX = Matx(1,0,0,0,cTauX,sTauX,0,-sTauX,cTauX); - Matx matRotY = Matx(cTauY,0,-sTauY,0,1,0,sTauY,0,cTauY); - Matx matRotXY = matRotY * matRotX; - Matx matProjZ = Matx(matRotXY(2,2),0,-matRotXY(0,2),0,matRotXY(2,2),-matRotXY(1,2),0,0,1); - if (matTilt) - { - // Matrix for trapezoidal distortion of tilted image sensor - *matTilt = matProjZ * matRotXY; - } - if (dMatTiltdTauX) - { - // Derivative with respect to tauX - Matx dMatRotXYdTauX = matRotY * Matx(0,0,0,0,-sTauX,cTauX,0,-cTauX,-sTauX); - Matx dMatProjZdTauX = Matx(dMatRotXYdTauX(2,2),0,-dMatRotXYdTauX(0,2), - 0,dMatRotXYdTauX(2,2),-dMatRotXYdTauX(1,2),0,0,0); - *dMatTiltdTauX = (matProjZ * dMatRotXYdTauX) + (dMatProjZdTauX * matRotXY); - } - if (dMatTiltdTauY) - { - // Derivative with respect to tauY - Matx dMatRotXYdTauY = Matx(-sTauY,0,-cTauY,0,0,0,cTauY,0,-sTauY) * matRotX; - Matx dMatProjZdTauY = Matx(dMatRotXYdTauY(2,2),0,-dMatRotXYdTauY(0,2), - 0,dMatRotXYdTauY(2,2),-dMatRotXYdTauY(1,2),0,0,0); - *dMatTiltdTauY = (matProjZ * dMatRotXYdTauY) + (dMatProjZdTauY * matRotXY); - } - if (invMatTilt) - { - FLOAT inv = 1./matRotXY(2,2); - Matx invMatProjZ = Matx(inv,0,inv*matRotXY(0,2),0,inv,inv*matRotXY(1,2),0,0,1); - *invMatTilt = matRotXY.t()*invMatProjZ; - } - } -} - -static const char* cvDistCoeffErr = "Distortion coefficients must be 1x4, 4x1, 1x5, 5x1, 1x8, 8x1, 1x12, 12x1, 1x14 or 14x1 floating-point vector"; - -static void _cvProjectPoints2Internal( const CvMat* objectPoints, - const CvMat* r_vec, - const CvMat* t_vec, - const CvMat* A, - const CvMat* distCoeffs, - CvMat* imagePoints, CvMat* dpdr CV_DEFAULT(NULL), - CvMat* dpdt CV_DEFAULT(NULL), CvMat* dpdf CV_DEFAULT(NULL), - CvMat* dpdc CV_DEFAULT(NULL), CvMat* dpdk CV_DEFAULT(NULL), - CvMat* dpdo CV_DEFAULT(NULL), - double aspectRatio CV_DEFAULT(0) ) -{ - Ptr matM, _m; - Ptr _dpdr, _dpdt, _dpdc, _dpdf, _dpdk; - Ptr _dpdo; - - int i, j, count; - int calc_derivatives; - const CvPoint3D64f* M; - CvPoint3D64f* m; - double r[3], R[9], dRdr[27], t[3], a[9], k[14] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0}, fx, fy, cx, cy; - Matx33d matTilt = Matx33d::eye(); - Matx33d dMatTiltdTauX(0,0,0,0,0,0,0,-1,0); - Matx33d dMatTiltdTauY(0,0,0,0,0,0,1,0,0); - CvMat _r, _t, _a = cvMat( 3, 3, CV_64F, a ), _k; - CvMat matR = cvMat( 3, 3, CV_64F, R ), _dRdr = cvMat( 3, 9, CV_64F, dRdr ); - double *dpdr_p = 0, *dpdt_p = 0, *dpdk_p = 0, *dpdf_p = 0, *dpdc_p = 0; - double* dpdo_p = 0; - int dpdr_step = 0, dpdt_step = 0, dpdk_step = 0, dpdf_step = 0, dpdc_step = 0; - int dpdo_step = 0; - bool fixedAspectRatio = aspectRatio > FLT_EPSILON; - - if( !CV_IS_MAT(objectPoints) || !CV_IS_MAT(r_vec) || - !CV_IS_MAT(t_vec) || !CV_IS_MAT(A) || - /*!CV_IS_MAT(distCoeffs) ||*/ !CV_IS_MAT(imagePoints) ) - CV_Error( CV_StsBadArg, "One of required arguments is not a valid matrix" ); - - int total = objectPoints->rows * objectPoints->cols * CV_MAT_CN(objectPoints->type); - if(total % 3 != 0) - { - //we have stopped support of homogeneous coordinates because it cause ambiguity in interpretation of the input data - CV_Error( CV_StsBadArg, "Homogeneous coordinates are not supported" ); - } - count = total / 3; - - if( CV_IS_CONT_MAT(objectPoints->type) && - (CV_MAT_DEPTH(objectPoints->type) == CV_32F || CV_MAT_DEPTH(objectPoints->type) == CV_64F)&& - ((objectPoints->rows == 1 && CV_MAT_CN(objectPoints->type) == 3) || - (objectPoints->rows == count && CV_MAT_CN(objectPoints->type)*objectPoints->cols == 3) || - (objectPoints->rows == 3 && CV_MAT_CN(objectPoints->type) == 1 && objectPoints->cols == count))) - { - matM.reset(cvCreateMat( objectPoints->rows, objectPoints->cols, CV_MAKETYPE(CV_64F,CV_MAT_CN(objectPoints->type)) )); - cvConvert(objectPoints, matM); - } - else - { -// matM = cvCreateMat( 1, count, CV_64FC3 ); -// cvConvertPointsHomogeneous( objectPoints, matM ); - CV_Error( CV_StsBadArg, "Homogeneous coordinates are not supported" ); - } - - if( CV_IS_CONT_MAT(imagePoints->type) && - (CV_MAT_DEPTH(imagePoints->type) == CV_32F || CV_MAT_DEPTH(imagePoints->type) == CV_64F) && - ((imagePoints->rows == 1 && CV_MAT_CN(imagePoints->type) == 3) || - (imagePoints->rows == count && CV_MAT_CN(imagePoints->type)*imagePoints->cols == 3) || - (imagePoints->rows == 3 && CV_MAT_CN(imagePoints->type) == 1 && imagePoints->cols == count))) - { - _m.reset(cvCreateMat( imagePoints->rows, imagePoints->cols, CV_MAKETYPE(CV_64F,CV_MAT_CN(imagePoints->type)) )); - cvConvert(imagePoints, _m); - } - else - { -// _m = cvCreateMat( 1, count, CV_64FC2 ); - CV_Error( CV_StsBadArg, "Homogeneous coordinates are not supported" ); - } - - M = (CvPoint3D64f*)matM->data.db; - m = (CvPoint3D64f*)_m->data.db; - - if( (CV_MAT_DEPTH(r_vec->type) != CV_64F && CV_MAT_DEPTH(r_vec->type) != CV_32F) || - (((r_vec->rows != 1 && r_vec->cols != 1) || - r_vec->rows*r_vec->cols*CV_MAT_CN(r_vec->type) != 3) && - ((r_vec->rows != 3 && r_vec->cols != 3) || CV_MAT_CN(r_vec->type) != 1))) - CV_Error( CV_StsBadArg, "Rotation must be represented by 1x3 or 3x1 " - "floating-point rotation vector, or 3x3 rotation matrix" ); - - if( r_vec->rows == 3 && r_vec->cols == 3 ) - { - _r = cvMat( 3, 1, CV_64FC1, r ); - cvRodrigues2( r_vec, &_r ); - cvRodrigues2( &_r, &matR, &_dRdr ); - cvCopy( r_vec, &matR ); - } - else - { - _r = cvMat( r_vec->rows, r_vec->cols, CV_MAKETYPE(CV_64F,CV_MAT_CN(r_vec->type)), r ); - cvConvert( r_vec, &_r ); - cvRodrigues2( &_r, &matR, &_dRdr ); - } - - if( (CV_MAT_DEPTH(t_vec->type) != CV_64F && CV_MAT_DEPTH(t_vec->type) != CV_32F) || - (t_vec->rows != 1 && t_vec->cols != 1) || - t_vec->rows*t_vec->cols*CV_MAT_CN(t_vec->type) != 3 ) - CV_Error( CV_StsBadArg, - "Translation vector must be 1x3 or 3x1 floating-point vector" ); - - _t = cvMat( t_vec->rows, t_vec->cols, CV_MAKETYPE(CV_64F,CV_MAT_CN(t_vec->type)), t ); - cvConvert( t_vec, &_t ); - - if( (CV_MAT_TYPE(A->type) != CV_64FC1 && CV_MAT_TYPE(A->type) != CV_32FC1) || - A->rows != 3 || A->cols != 3 ) - CV_Error( CV_StsBadArg, "Instrinsic parameters must be 3x3 floating-point matrix" ); - - cvConvert( A, &_a ); - fx = a[0]; fy = a[4]; - cx = a[2]; cy = a[5]; - - if( fixedAspectRatio ) - fx = fy*aspectRatio; - - if( distCoeffs ) - { - if( !CV_IS_MAT(distCoeffs) || - (CV_MAT_DEPTH(distCoeffs->type) != CV_64F && - CV_MAT_DEPTH(distCoeffs->type) != CV_32F) || - (distCoeffs->rows != 1 && distCoeffs->cols != 1) || - (distCoeffs->rows*distCoeffs->cols*CV_MAT_CN(distCoeffs->type) != 4 && - distCoeffs->rows*distCoeffs->cols*CV_MAT_CN(distCoeffs->type) != 5 && - distCoeffs->rows*distCoeffs->cols*CV_MAT_CN(distCoeffs->type) != 8 && - distCoeffs->rows*distCoeffs->cols*CV_MAT_CN(distCoeffs->type) != 12 && - distCoeffs->rows*distCoeffs->cols*CV_MAT_CN(distCoeffs->type) != 14) ) - CV_Error( CV_StsBadArg, cvDistCoeffErr ); - - _k = cvMat( distCoeffs->rows, distCoeffs->cols, - CV_MAKETYPE(CV_64F,CV_MAT_CN(distCoeffs->type)), k ); - cvConvert( distCoeffs, &_k ); - if(k[12] != 0 || k[13] != 0) - { - _detail::computeTiltProjectionMatrix(k[12], k[13], - &matTilt, &dMatTiltdTauX, &dMatTiltdTauY); - } - } - - if( dpdr ) - { - if( !CV_IS_MAT(dpdr) || - (CV_MAT_TYPE(dpdr->type) != CV_32FC1 && - CV_MAT_TYPE(dpdr->type) != CV_64FC1) || - dpdr->rows != count*2 || dpdr->cols != 3 ) - CV_Error( CV_StsBadArg, "dp/drot must be 2Nx3 floating-point matrix" ); - - if( CV_MAT_TYPE(dpdr->type) == CV_64FC1 ) - { - _dpdr.reset(cvCloneMat(dpdr)); - } - else - _dpdr.reset(cvCreateMat( 2*count, 3, CV_64FC1 )); - dpdr_p = _dpdr->data.db; - dpdr_step = _dpdr->step/sizeof(dpdr_p[0]); - } - - if( dpdt ) - { - if( !CV_IS_MAT(dpdt) || - (CV_MAT_TYPE(dpdt->type) != CV_32FC1 && - CV_MAT_TYPE(dpdt->type) != CV_64FC1) || - dpdt->rows != count*2 || dpdt->cols != 3 ) - CV_Error( CV_StsBadArg, "dp/dT must be 2Nx3 floating-point matrix" ); - - if( CV_MAT_TYPE(dpdt->type) == CV_64FC1 ) - { - _dpdt.reset(cvCloneMat(dpdt)); - } - else - _dpdt.reset(cvCreateMat( 2*count, 3, CV_64FC1 )); - dpdt_p = _dpdt->data.db; - dpdt_step = _dpdt->step/sizeof(dpdt_p[0]); - } - - if( dpdf ) - { - if( !CV_IS_MAT(dpdf) || - (CV_MAT_TYPE(dpdf->type) != CV_32FC1 && CV_MAT_TYPE(dpdf->type) != CV_64FC1) || - dpdf->rows != count*2 || dpdf->cols != 2 ) - CV_Error( CV_StsBadArg, "dp/df must be 2Nx2 floating-point matrix" ); - - if( CV_MAT_TYPE(dpdf->type) == CV_64FC1 ) - { - _dpdf.reset(cvCloneMat(dpdf)); - } - else - _dpdf.reset(cvCreateMat( 2*count, 2, CV_64FC1 )); - dpdf_p = _dpdf->data.db; - dpdf_step = _dpdf->step/sizeof(dpdf_p[0]); - } - - if( dpdc ) - { - if( !CV_IS_MAT(dpdc) || - (CV_MAT_TYPE(dpdc->type) != CV_32FC1 && CV_MAT_TYPE(dpdc->type) != CV_64FC1) || - dpdc->rows != count*2 || dpdc->cols != 2 ) - CV_Error( CV_StsBadArg, "dp/dc must be 2Nx2 floating-point matrix" ); - - if( CV_MAT_TYPE(dpdc->type) == CV_64FC1 ) - { - _dpdc.reset(cvCloneMat(dpdc)); - } - else - _dpdc.reset(cvCreateMat( 2*count, 2, CV_64FC1 )); - dpdc_p = _dpdc->data.db; - dpdc_step = _dpdc->step/sizeof(dpdc_p[0]); - } - - if( dpdk ) - { - if( !CV_IS_MAT(dpdk) || - (CV_MAT_TYPE(dpdk->type) != CV_32FC1 && CV_MAT_TYPE(dpdk->type) != CV_64FC1) || - dpdk->rows != count*2 || (dpdk->cols != 14 && dpdk->cols != 12 && dpdk->cols != 8 && dpdk->cols != 5 && dpdk->cols != 4 && dpdk->cols != 2) ) - CV_Error( CV_StsBadArg, "dp/df must be 2Nx14, 2Nx12, 2Nx8, 2Nx5, 2Nx4 or 2Nx2 floating-point matrix" ); - - if( !distCoeffs ) - CV_Error( CV_StsNullPtr, "distCoeffs is NULL while dpdk is not" ); - - if( CV_MAT_TYPE(dpdk->type) == CV_64FC1 ) - { - _dpdk.reset(cvCloneMat(dpdk)); - } - else - _dpdk.reset(cvCreateMat( dpdk->rows, dpdk->cols, CV_64FC1 )); - dpdk_p = _dpdk->data.db; - dpdk_step = _dpdk->step/sizeof(dpdk_p[0]); - } - - if( dpdo ) - { - if( !CV_IS_MAT( dpdo ) || ( CV_MAT_TYPE( dpdo->type ) != CV_32FC1 - && CV_MAT_TYPE( dpdo->type ) != CV_64FC1 ) - || dpdo->rows != count * 2 || dpdo->cols != count * 3 ) - CV_Error( CV_StsBadArg, "dp/do must be 2Nx3N floating-point matrix" ); - - if( CV_MAT_TYPE( dpdo->type ) == CV_64FC1 ) - { - _dpdo.reset( cvCloneMat( dpdo ) ); - } - else - _dpdo.reset( cvCreateMat( 2 * count, 3 * count, CV_64FC1 ) ); - cvZero(_dpdo); - dpdo_p = _dpdo->data.db; - dpdo_step = _dpdo->step / sizeof( dpdo_p[0] ); - } - - calc_derivatives = dpdr || dpdt || dpdf || dpdc || dpdk || dpdo; - - for( i = 0; i < count; i++ ) - { - double X = M[i].x, Y = M[i].y, Z = M[i].z; - double x = R[0]*X + R[1]*Y + R[2]*Z + t[0]; - double y = R[3]*X + R[4]*Y + R[5]*Z + t[1]; - double z = R[6]*X + R[7]*Y + R[8]*Z + t[2]; - double r2, r4, r6, a1, a2, a3, cdist, icdist2; - double xd, yd, xd0, yd0, invProj; - Vec3d vecTilt; - Vec3d dVecTilt; - Matx22d dMatTilt; - Vec2d dXdYd; - - double z0 = z; - z = z ? 1./z : 1; - x *= z; y *= z; - - r2 = x*x + y*y; - r4 = r2*r2; - r6 = r4*r2; - a1 = 2*x*y; - a2 = r2 + 2*x*x; - a3 = r2 + 2*y*y; - cdist = 1 + k[0]*r2 + k[1]*r4 + k[4]*r6; - icdist2 = 1./(1 + k[5]*r2 + k[6]*r4 + k[7]*r6); - xd0 = x*cdist*icdist2 + k[2]*a1 + k[3]*a2 + k[8]*r2+k[9]*r4; - yd0 = y*cdist*icdist2 + k[2]*a3 + k[3]*a1 + k[10]*r2+k[11]*r4; - - // additional distortion by projecting onto a tilt plane - vecTilt = matTilt*Vec3d(xd0, yd0, 1); - invProj = vecTilt(2) ? 1./vecTilt(2) : 1; - xd = invProj * vecTilt(0); - yd = invProj * vecTilt(1); - - m[i].x = xd*fx + cx; - m[i].y = yd*fy + cy; - m[i].z = z; // Just put the projected Z coordinate here, we mainly care about the sign - - if( calc_derivatives ) - { - if( dpdc_p ) - { - dpdc_p[0] = 1; dpdc_p[1] = 0; // dp_xdc_x; dp_xdc_y - dpdc_p[dpdc_step] = 0; - dpdc_p[dpdc_step+1] = 1; - dpdc_p += dpdc_step*2; - } - - if( dpdf_p ) - { - if( fixedAspectRatio ) - { - dpdf_p[0] = 0; dpdf_p[1] = xd*aspectRatio; // dp_xdf_x; dp_xdf_y - dpdf_p[dpdf_step] = 0; - dpdf_p[dpdf_step+1] = yd; - } - else - { - dpdf_p[0] = xd; dpdf_p[1] = 0; - dpdf_p[dpdf_step] = 0; - dpdf_p[dpdf_step+1] = yd; - } - dpdf_p += dpdf_step*2; - } - for (int row = 0; row < 2; ++row) - for (int col = 0; col < 2; ++col) - dMatTilt(row,col) = matTilt(row,col)*vecTilt(2) - - matTilt(2,col)*vecTilt(row); - double invProjSquare = (invProj*invProj); - dMatTilt *= invProjSquare; - if( dpdk_p ) - { - dXdYd = dMatTilt*Vec2d(x*icdist2*r2, y*icdist2*r2); - dpdk_p[0] = fx*dXdYd(0); - dpdk_p[dpdk_step] = fy*dXdYd(1); - dXdYd = dMatTilt*Vec2d(x*icdist2*r4, y*icdist2*r4); - dpdk_p[1] = fx*dXdYd(0); - dpdk_p[dpdk_step+1] = fy*dXdYd(1); - if( _dpdk->cols > 2 ) - { - dXdYd = dMatTilt*Vec2d(a1, a3); - dpdk_p[2] = fx*dXdYd(0); - dpdk_p[dpdk_step+2] = fy*dXdYd(1); - dXdYd = dMatTilt*Vec2d(a2, a1); - dpdk_p[3] = fx*dXdYd(0); - dpdk_p[dpdk_step+3] = fy*dXdYd(1); - if( _dpdk->cols > 4 ) - { - dXdYd = dMatTilt*Vec2d(x*icdist2*r6, y*icdist2*r6); - dpdk_p[4] = fx*dXdYd(0); - dpdk_p[dpdk_step+4] = fy*dXdYd(1); - - if( _dpdk->cols > 5 ) - { - dXdYd = dMatTilt*Vec2d( - x*cdist*(-icdist2)*icdist2*r2, y*cdist*(-icdist2)*icdist2*r2); - dpdk_p[5] = fx*dXdYd(0); - dpdk_p[dpdk_step+5] = fy*dXdYd(1); - dXdYd = dMatTilt*Vec2d( - x*cdist*(-icdist2)*icdist2*r4, y*cdist*(-icdist2)*icdist2*r4); - dpdk_p[6] = fx*dXdYd(0); - dpdk_p[dpdk_step+6] = fy*dXdYd(1); - dXdYd = dMatTilt*Vec2d( - x*cdist*(-icdist2)*icdist2*r6, y*cdist*(-icdist2)*icdist2*r6); - dpdk_p[7] = fx*dXdYd(0); - dpdk_p[dpdk_step+7] = fy*dXdYd(1); - if( _dpdk->cols > 8 ) - { - dXdYd = dMatTilt*Vec2d(r2, 0); - dpdk_p[8] = fx*dXdYd(0); //s1 - dpdk_p[dpdk_step+8] = fy*dXdYd(1); //s1 - dXdYd = dMatTilt*Vec2d(r4, 0); - dpdk_p[9] = fx*dXdYd(0); //s2 - dpdk_p[dpdk_step+9] = fy*dXdYd(1); //s2 - dXdYd = dMatTilt*Vec2d(0, r2); - dpdk_p[10] = fx*dXdYd(0);//s3 - dpdk_p[dpdk_step+10] = fy*dXdYd(1); //s3 - dXdYd = dMatTilt*Vec2d(0, r4); - dpdk_p[11] = fx*dXdYd(0);//s4 - dpdk_p[dpdk_step+11] = fy*dXdYd(1); //s4 - if( _dpdk->cols > 12 ) - { - dVecTilt = dMatTiltdTauX * Vec3d(xd0, yd0, 1); - dpdk_p[12] = fx * invProjSquare * ( - dVecTilt(0) * vecTilt(2) - dVecTilt(2) * vecTilt(0)); - dpdk_p[dpdk_step+12] = fy*invProjSquare * ( - dVecTilt(1) * vecTilt(2) - dVecTilt(2) * vecTilt(1)); - dVecTilt = dMatTiltdTauY * Vec3d(xd0, yd0, 1); - dpdk_p[13] = fx * invProjSquare * ( - dVecTilt(0) * vecTilt(2) - dVecTilt(2) * vecTilt(0)); - dpdk_p[dpdk_step+13] = fy * invProjSquare * ( - dVecTilt(1) * vecTilt(2) - dVecTilt(2) * vecTilt(1)); - } - } - } - } - } - dpdk_p += dpdk_step*2; - } - - if( dpdt_p ) - { - double dxdt[] = { z, 0, -x*z }, dydt[] = { 0, z, -y*z }; - for( j = 0; j < 3; j++ ) - { - double dr2dt = 2*x*dxdt[j] + 2*y*dydt[j]; - double dcdist_dt = k[0]*dr2dt + 2*k[1]*r2*dr2dt + 3*k[4]*r4*dr2dt; - double dicdist2_dt = -icdist2*icdist2*(k[5]*dr2dt + 2*k[6]*r2*dr2dt + 3*k[7]*r4*dr2dt); - double da1dt = 2*(x*dydt[j] + y*dxdt[j]); - double dmxdt = (dxdt[j]*cdist*icdist2 + x*dcdist_dt*icdist2 + x*cdist*dicdist2_dt + - k[2]*da1dt + k[3]*(dr2dt + 4*x*dxdt[j]) + k[8]*dr2dt + 2*r2*k[9]*dr2dt); - double dmydt = (dydt[j]*cdist*icdist2 + y*dcdist_dt*icdist2 + y*cdist*dicdist2_dt + - k[2]*(dr2dt + 4*y*dydt[j]) + k[3]*da1dt + k[10]*dr2dt + 2*r2*k[11]*dr2dt); - dXdYd = dMatTilt*Vec2d(dmxdt, dmydt); - dpdt_p[j] = fx*dXdYd(0); - dpdt_p[dpdt_step+j] = fy*dXdYd(1); - } - dpdt_p += dpdt_step*2; - } - - if( dpdr_p ) - { - double dx0dr[] = - { - X*dRdr[0] + Y*dRdr[1] + Z*dRdr[2], - X*dRdr[9] + Y*dRdr[10] + Z*dRdr[11], - X*dRdr[18] + Y*dRdr[19] + Z*dRdr[20] - }; - double dy0dr[] = - { - X*dRdr[3] + Y*dRdr[4] + Z*dRdr[5], - X*dRdr[12] + Y*dRdr[13] + Z*dRdr[14], - X*dRdr[21] + Y*dRdr[22] + Z*dRdr[23] - }; - double dz0dr[] = - { - X*dRdr[6] + Y*dRdr[7] + Z*dRdr[8], - X*dRdr[15] + Y*dRdr[16] + Z*dRdr[17], - X*dRdr[24] + Y*dRdr[25] + Z*dRdr[26] - }; - for( j = 0; j < 3; j++ ) - { - double dxdr = z*(dx0dr[j] - x*dz0dr[j]); - double dydr = z*(dy0dr[j] - y*dz0dr[j]); - double dr2dr = 2*x*dxdr + 2*y*dydr; - double dcdist_dr = (k[0] + 2*k[1]*r2 + 3*k[4]*r4)*dr2dr; - double dicdist2_dr = -icdist2*icdist2*(k[5] + 2*k[6]*r2 + 3*k[7]*r4)*dr2dr; - double da1dr = 2*(x*dydr + y*dxdr); - double dmxdr = (dxdr*cdist*icdist2 + x*dcdist_dr*icdist2 + x*cdist*dicdist2_dr + - k[2]*da1dr + k[3]*(dr2dr + 4*x*dxdr) + (k[8] + 2*r2*k[9])*dr2dr); - double dmydr = (dydr*cdist*icdist2 + y*dcdist_dr*icdist2 + y*cdist*dicdist2_dr + - k[2]*(dr2dr + 4*y*dydr) + k[3]*da1dr + (k[10] + 2*r2*k[11])*dr2dr); - dXdYd = dMatTilt*Vec2d(dmxdr, dmydr); - dpdr_p[j] = fx*dXdYd(0); - dpdr_p[dpdr_step+j] = fy*dXdYd(1); - } - dpdr_p += dpdr_step*2; - } - - if( dpdo_p ) - { - double dxdo[] = { z * ( R[0] - x * z * z0 * R[6] ), - z * ( R[1] - x * z * z0 * R[7] ), - z * ( R[2] - x * z * z0 * R[8] ) }; - double dydo[] = { z * ( R[3] - y * z * z0 * R[6] ), - z * ( R[4] - y * z * z0 * R[7] ), - z * ( R[5] - y * z * z0 * R[8] ) }; - for( j = 0; j < 3; j++ ) - { - double dr2do = 2 * x * dxdo[j] + 2 * y * dydo[j]; - double dr4do = 2 * r2 * dr2do; - double dr6do = 3 * r4 * dr2do; - double da1do = 2 * y * dxdo[j] + 2 * x * dydo[j]; - double da2do = dr2do + 4 * x * dxdo[j]; - double da3do = dr2do + 4 * y * dydo[j]; - double dcdist_do - = k[0] * dr2do + k[1] * dr4do + k[4] * dr6do; - double dicdist2_do = -icdist2 * icdist2 - * ( k[5] * dr2do + k[6] * dr4do + k[7] * dr6do ); - double dxd0_do = cdist * icdist2 * dxdo[j] - + x * icdist2 * dcdist_do + x * cdist * dicdist2_do - + k[2] * da1do + k[3] * da2do + k[8] * dr2do - + k[9] * dr4do; - double dyd0_do = cdist * icdist2 * dydo[j] - + y * icdist2 * dcdist_do + y * cdist * dicdist2_do - + k[2] * da3do + k[3] * da1do + k[10] * dr2do - + k[11] * dr4do; - dXdYd = dMatTilt * Vec2d( dxd0_do, dyd0_do ); - dpdo_p[i * 3 + j] = fx * dXdYd( 0 ); - dpdo_p[dpdo_step + i * 3 + j] = fy * dXdYd( 1 ); - } - dpdo_p += dpdo_step * 2; - } - } - } - - if( _m != imagePoints ) - cvConvert( _m, imagePoints ); - - if( _dpdr != dpdr ) - cvConvert( _dpdr, dpdr ); - - if( _dpdt != dpdt ) - cvConvert( _dpdt, dpdt ); - - if( _dpdf != dpdf ) - cvConvert( _dpdf, dpdf ); - - if( _dpdc != dpdc ) - cvConvert( _dpdc, dpdc ); - - if( _dpdk != dpdk ) - cvConvert( _dpdk, dpdk ); - - if( _dpdo != dpdo ) - cvConvert( _dpdo, dpdo ); -} - -static void _cvProjectPoints2( const CvMat* objectPoints, - const CvMat* r_vec, - const CvMat* t_vec, - const CvMat* A, - const CvMat* distCoeffs, - CvMat* imagePoints, CvMat* dpdr, - CvMat* dpdt, CvMat* dpdf, - CvMat* dpdc, CvMat* dpdk, - double aspectRatio ) -{ - _cvProjectPoints2Internal( objectPoints, r_vec, t_vec, A, distCoeffs, imagePoints, dpdr, dpdt, - dpdf, dpdc, dpdk, NULL, aspectRatio ); } diff --git a/aruco_pose/vendor/VendorOpenCV.cmake b/aruco_pose/vendor/VendorOpenCV.cmake index fc0bac5f..4b8f44bb 100644 --- a/aruco_pose/vendor/VendorOpenCV.cmake +++ b/aruco_pose/vendor/VendorOpenCV.cmake @@ -7,6 +7,7 @@ endif() message(STATUS "Adding vendored aruco_pose OpenCV module") add_library(_opencv_aruco STATIC + vendor/aruco/src/apriltag_quad_thresh.cpp vendor/aruco/src/aruco.cpp vendor/aruco/src/charuco.cpp vendor/aruco/src/dictionary.cpp @@ -23,7 +24,7 @@ target_compile_definitions(_opencv_aruco PRIVATE CV_OVERRIDE=override ) target_compile_options(_opencv_aruco PRIVATE - -fpic -fPIC + -fpic -fPIC -fvisibility=hidden ) target_include_directories(_opencv_aruco PUBLIC diff --git a/aruco_pose/vendor/aruco/src/apriltag_quad_thresh.cpp b/aruco_pose/vendor/aruco/src/apriltag_quad_thresh.cpp index fc75a384..06170619 100644 --- a/aruco_pose/vendor/aruco/src/apriltag_quad_thresh.cpp +++ b/aruco_pose/vendor/aruco/src/apriltag_quad_thresh.cpp @@ -94,7 +94,7 @@ void ptsort_(struct pt *pts, int sz) // Use stack storage if it's not too big. cv::AutoBuffer _tmp_stack(sz); - memcpy(_tmp_stack.data(), pts, sizeof(struct pt) * sz); + memcpy(_tmp_stack, pts, sizeof(struct pt) * sz); int asz = sz/2; int bsz = sz - asz; @@ -470,11 +470,11 @@ int quad_segment_agg(int sz, struct line_fit_pt *lfps, int indices[4]){ int rvalloc_pos = 0; int rvalloc_size = 3*sz; cv::AutoBuffer rvalloc_(std::max(1, rvalloc_size)); - memset(rvalloc_.data(), 0, sizeof(rvalloc_[0]) * rvalloc_.size()); // TODO Add AutoBuffer zero fill - struct remove_vertex *rvalloc = rvalloc_.data(); + memset(rvalloc_, 0, sizeof(rvalloc_[0]) * rvalloc_.size()); // TODO Add AutoBuffer zero fill + struct remove_vertex *rvalloc = rvalloc_; cv::AutoBuffer segs_(std::max(1, sz)); // TODO Add AutoBuffer zero fill - memset(segs_.data(), 0, sizeof(segs_[0]) * segs_.size()); - struct segment *segs = segs_.data(); + memset(segs_, 0, sizeof(segs_[0]) * segs_.size()); + struct segment *segs = segs_; // populate with initial entries for (int i = 0; i < sz; i++) { @@ -753,8 +753,8 @@ int fit_quad(const Ptr &_params, const Mat im, zarray_t *clu // efficiently computed for any contiguous range of indices. cv::AutoBuffer lfps_(sz); - memset(lfps_.data(), 0, sizeof(lfps_[0]) * lfps_.size()); // TODO Add AutoBuffer zero fill - struct line_fit_pt *lfps = lfps_.data(); + memset(lfps_, 0, sizeof(lfps_[0]) * lfps_.size()); // TODO Add AutoBuffer zero fill + struct line_fit_pt *lfps = lfps_; for (int i = 0; i < sz; i++) { struct pt *p; diff --git a/builder/assets/clever/setup.py b/builder/assets/clever/setup.py index 1ed75dee..d2169cdb 100755 --- a/builder/assets/clever/setup.py +++ b/builder/assets/clever/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from distutils.core import setup diff --git a/builder/assets/noetic-rosdep-clover.yaml b/builder/assets/noetic-rosdep-clover.yaml new file mode 100644 index 00000000..a595b87e --- /dev/null +++ b/builder/assets/noetic-rosdep-clover.yaml @@ -0,0 +1,18 @@ +async_web_server_cpp: + debian: + buster: [ros-noetic-async-web-server-cpp] +led_msgs: + debian: + buster: [ros-noetic-led-msgs] +ros_pytest: + debian: + buster: [ros-noetic-ros-pytest] +tf2_web_republisher: + debian: + buster: [ros-noetic-tf2-web-republisher] +web_video_server: + debian: + buster: [ros-noetic-web-video-server] +ws281x: + debian: + buster: [ros-noetic-ws281x] diff --git a/builder/assets/roscore.service b/builder/assets/roscore.service index 81718289..b749e887 100644 --- a/builder/assets/roscore.service +++ b/builder/assets/roscore.service @@ -3,7 +3,7 @@ Description=Launcher for the ROS master, parameter server and rosout logging nod [Service] User=pi -ExecStart=/bin/sh -c ". /opt/ros/melodic/setup.sh; ROS_HOSTNAME=`hostname`.local exec roscore" +ExecStart=/bin/sh -c ". /opt/ros/noetic/setup.sh; ROS_HOSTNAME=`hostname`.local exec roscore" Restart=on-failure RestartSec=3 diff --git a/builder/image-build.sh b/builder/image-build.sh index db7be310..082b01a7 100755 --- a/builder/image-build.sh +++ b/builder/image-build.sh @@ -117,7 +117,7 @@ ${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/avahi-s # Clover ${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/clover.service' '/lib/systemd/system/' ${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/roscore.service' '/lib/systemd/system/' -${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/melodic-rosdep-clover.yaml' '/etc/ros/rosdep/' +${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/noetic-rosdep-clover.yaml' '/etc/ros/rosdep/' ${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/ros_python_paths' '/etc/sudoers.d/' ${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/pigpiod.service' '/lib/systemd/system/' ${BUILDER_DIR}/image-chroot.sh ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/launch.nanorc' '/usr/share/nano/' diff --git a/builder/image-ros.sh b/builder/image-ros.sh index b9700088..b8016083 100755 --- a/builder/image-ros.sh +++ b/builder/image-ros.sh @@ -21,6 +21,9 @@ INSTALL_ROS_PACK_SOURCES=$3 DISCOVER_ROS_PACK=$4 NUMBER_THREADS=$5 +# Current ROS distribution +ROS_DISTRO=noetic + echo_stamp() { # TEMPLATE: echo_stamp # TYPE: SUCCESS, ERROR, INFO @@ -68,7 +71,8 @@ my_travis_retry() { # TODO: 'kinetic-rosdep-clover.yaml' should add only if we use our repo? echo_stamp "Init rosdep" my_travis_retry rosdep init -echo "yaml file:///etc/ros/rosdep/melodic-rosdep-clover.yaml" >> /etc/ros/rosdep/sources.list.d/20-default.list +# FIXME: Re-add this after missing packages are built +echo "yaml file:///etc/ros/rosdep/${ROS_DISTRO}-rosdep-clover.yaml" >> /etc/ros/rosdep/sources.list.d/20-default.list my_travis_retry rosdep update echo_stamp "Populate rosdep for ROS user" @@ -76,22 +80,39 @@ my_travis_retry sudo -u pi rosdep update export ROS_IP='127.0.0.1' # needed for running tests -# echo_stamp "Reconfiguring Clover repository for simplier unshallowing" # TODO: bring back -# cd /home/pi/catkin_ws/src/clover -# git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" -echo_stamp "Remove .git from Clover to reduce the size" -rm -rf /home/pi/catkin_ws/src/clover/.git # TODO: remove +# echo_stamp "Reconfiguring Clover repository for simplier unshallowing" +cd /home/pi/catkin_ws/src/clover +git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + +# This is sort of a hack to force "custom" packages to be installed - the ones built by COEX, linked against OpenCV 4.2 +# I **wish** OpenCV would not be such a mess, but, well, here we are. +echo_stamp "Installing OpenCV 4.2-compatible ROS packages" +apt install -y --no-install-recommends \ +ros-${ROS_DISTRO}-compressed-image-transport=1.14.0-0buster \ +ros-${ROS_DISTRO}-cv-bridge=1.15.0-0buster \ +ros-${ROS_DISTRO}-cv-camera=0.5.0-0buster \ +ros-${ROS_DISTRO}-image-publisher=1.15.3-0buster \ +ros-${ROS_DISTRO}-web-video-server=0.2.1-0buster +apt-mark hold \ +ros-${ROS_DISTRO}-compressed-image-transport \ +ros-${ROS_DISTRO}-cv-bridge \ +ros-${ROS_DISTRO}-cv-camera \ +ros-${ROS_DISTRO}-image-publisher \ +ros-${ROS_DISTRO}-web-video-server + +echo_stamp "Installing libboost-dev" # https://travis-ci.org/github/CopterExpress/clover/jobs/766318908#L6536 +my_travis_retry apt-get install -y --no-install-recommends libboost-dev libboost-all-dev echo_stamp "Build and install Clover" cd /home/pi/catkin_ws # Don't try to install gazebo_ros -my_travis_retry rosdep install -y --from-paths src --ignore-src --rosdistro melodic --os=debian:buster \ +my_travis_retry rosdep install -y --from-paths src --ignore-src --rosdistro ${ROS_DISTRO} --os=debian:buster \ --skip-keys=gazebo_ros --skip-keys=gazebo_plugins -my_travis_retry pip install wheel -my_travis_retry pip install -r /home/pi/catkin_ws/src/clover/clover/requirements.txt -source /opt/ros/melodic/setup.bash +my_travis_retry pip3 install wheel +my_travis_retry pip3 install -r /home/pi/catkin_ws/src/clover/clover/requirements.txt +source /opt/ros/${ROS_DISTRO}/setup.bash # Don't build simulation plugins for actual drone -catkin_make -j2 -DCMAKE_BUILD_TYPE=Release -DCATKIN_BLACKLIST_PACKAGES=clover_gazebo_plugins +catkin_make -j2 -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCATKIN_BLACKLIST_PACKAGES=clover_gazebo_plugins echo_stamp "Install clever package (for backwards compatibility)" cd /home/pi/catkin_ws/src/clover/builder/assets/clever @@ -108,23 +129,20 @@ touch node_modules/CATKIN_IGNORE docs/CATKIN_IGNORE _book/CATKIN_IGNORE clover/w echo_stamp "Installing additional ROS packages" my_travis_retry apt-get install -y --no-install-recommends \ - ros-melodic-dynamic-reconfigure \ - ros-melodic-compressed-image-transport \ - ros-melodic-rosbridge-suite \ - ros-melodic-rosserial \ - ros-melodic-usb-cam \ - ros-melodic-vl53l1x \ - ros-melodic-ws281x \ - ros-melodic-rosshow + ros-${ROS_DISTRO}-dynamic-reconfigure \ + ros-${ROS_DISTRO}-rosbridge-suite \ + ros-${ROS_DISTRO}-rosserial \ + ros-${ROS_DISTRO}-usb-cam \ + ros-${ROS_DISTRO}-vl53l1x \ + ros-${ROS_DISTRO}-ws281x \ + ros-${ROS_DISTRO}-rosshow \ + ros-${ROS_DISTRO}-cmake-modules \ + ros-${ROS_DISTRO}-image-view # TODO move GeographicLib datasets to Mavros debian package echo_stamp "Install GeographicLib datasets (needed for mavros)" \ && wget -qO- https://raw.githubusercontent.com/mavlink/mavros/master/mavros/scripts/install_geographiclib_datasets.sh | bash -# FIXME: Buster comes with tornado==5.1.1 but we need tornado==4.2.1 for rosbridge_suite -# (note that Python 3 will still have a more recent version) -pip install tornado==4.2.1 - echo_stamp "Running tests" cd /home/pi/catkin_ws # FIXME: Investigate failing tests @@ -141,7 +159,7 @@ cat << EOF >> /home/pi/.bashrc LANG='C.UTF-8' LC_ALL='C.UTF-8' export ROS_HOSTNAME=\`hostname\`.local -source /opt/ros/melodic/setup.bash +source /opt/ros/${ROS_DISTRO}/setup.bash source /home/pi/catkin_ws/devel/setup.bash EOF diff --git a/builder/image-software.sh b/builder/image-software.sh index d0686b2e..c675c913 100755 --- a/builder/image-software.sh +++ b/builder/image-software.sh @@ -64,15 +64,14 @@ echo "APT::Acquire::Retries \"3\";" > /etc/apt/apt.conf.d/80-retries echo_stamp "Install apt keys & repos" # TODO: This STDOUT consist 'OK' -curl http://deb.coex.tech/aptly_repo_signing.key 2> /dev/null | apt-key add - apt-get update \ && apt-get install --no-install-recommends -y dirmngr > /dev/null \ && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 echo "deb http://packages.ros.org/ros/ubuntu buster main" > /etc/apt/sources.list.d/ros-latest.list -echo "deb http://deb.coex.tech/opencv3 buster main" > /etc/apt/sources.list.d/opencv3.list -echo "deb http://deb.coex.tech/rpi-ros-melodic buster main" > /etc/apt/sources.list.d/rpi-ros-melodic.list -echo "deb http://deb.coex.tech/clover buster main" > /etc/apt/sources.list.d/clover.list + +wget -O - 'http://packages.coex.tech/key.asc' | apt-key add - +echo 'deb http://packages.coex.tech buster main' >> /etc/apt/sources.list echo_stamp "Update apt cache" @@ -99,18 +98,18 @@ tree \ vim \ libjpeg8 \ tcpdump \ -ltrace \ libpoco-dev \ libzbar0 \ -python-rosdep \ -python-rosinstall-generator \ -python-wstool \ -python-rosinstall \ +python3-rosdep \ +python3-rosinstall-generator \ +python3-wstool \ +python3-rosinstall \ build-essential \ libffi-dev \ monkey \ pigpio python-pigpio python3-pigpio \ i2c-tools \ +espeak espeak-data python-espeak python3-espeak \ ntpdate \ python-dev \ python3-dev \ @@ -144,7 +143,7 @@ my_travis_retry pip3 install butterfly[systemd] systemctl enable butterfly.socket echo_stamp "Install ws281x library" -my_travis_retry pip install --prefer-binary rpi_ws281x +my_travis_retry pip3 install --prefer-binary rpi_ws281x echo_stamp "Setup Monkey" mv /etc/monkey/sites/default /etc/monkey/sites/default.orig diff --git a/builder/image-validate.sh b/builder/image-validate.sh index a95beecd..8d13f0f6 100755 --- a/builder/image-validate.sh +++ b/builder/image-validate.sh @@ -16,16 +16,20 @@ set -ex echo "Run image tests" -export ROS_DISTRO='melodic' +export ROS_DISTRO='noetic' export ROS_IP='127.0.0.1' -source /opt/ros/melodic/setup.bash +source /opt/ros/${ROS_DISTRO}/setup.bash source /home/pi/catkin_ws/devel/setup.bash +systemctl start roscore cd /home/pi/catkin_ws/src/clover/builder/test/ ./tests.sh ./tests.py ./tests_py3.py +[[ $(./test_qr.py) == "Found QRCODE with data Проверка Unicode with center at x=66.0, y=66.0" ]] [[ $(./tests_clever.py) == "Warning: clever package is renamed to clover" ]] # test backwards compatibility +systemctl stop roscore + echo "Move /etc/ld.so.preload back to its original position" mv /etc/ld.so.preload.disabled-for-build /etc/ld.so.preload diff --git a/builder/standalone-install.sh b/builder/standalone-install.sh index e826401a..7a8ae535 100755 --- a/builder/standalone-install.sh +++ b/builder/standalone-install.sh @@ -6,21 +6,40 @@ set -e apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 # https://github.com/osrf/docker_images/issues/535 apt-get update apt-get install -y curl -curl https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py -python ./get-pip.py +if [ "x${ROS_PYTHON_VERSION}" = "x3" ]; then + PYTHON=python3 + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py +else + PYTHON=python + curl https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py +fi +${PYTHON} ./get-pip.py # Step 1.5: Add deb.coex.tech to apt curl http://deb.coex.tech/aptly_repo_signing.key 2> /dev/null | apt-key add - echo "deb http://deb.coex.tech/ros xenial main" > /etc/apt/sources.list.d/coex.tech.list echo "yaml file:///etc/ros/rosdep/coex.yaml" > /etc/ros/rosdep/sources.list.d/99-coex.list +CODENAME=$(lsb_release -sc) + cat < /etc/ros/rosdep/coex.yaml led_msgs: ubuntu: - xenial: ros-kinetic-led-msgs - bionic: ros-melodic-led-msgs - debian: - stretch: ros-kinetic-led-msgs - buster: ros-melodic-led-msgs + ${CODENAME}: [ros-${ROS_DISTRO}-led-msgs] +async_web_server_cpp: + ubuntu: + ${CODENAME}: [ros-${ROS_DISTRO}-async-web-server-cpp] +ros_pytest: + ubuntu: + ${CODENAME}: [ros-${ROS_DISTRO}-ros-pytest] +tf2_web_republisher: + ubuntu: + ${CODENAME}: [ros-${ROS_DISTRO}-tf2-web-republisher] +web_video_server: + ubuntu: + ${CODENAME}: [ros-${ROS_DISTRO}-web-video-server] +ws281x: + ubuntu: + ${CODENAME}: [ros-${ROS_DISTRO}-ws281x] EOF apt-get update rosdep update @@ -38,7 +57,7 @@ cd /root/catkin_ws catkin_make # Step 4: Run tests -pip install --upgrade pytest +${PYTHON} -m pip install --upgrade pytest cd /root/catkin_ws source devel/setup.bash catkin_make run_tests && catkin_test_results diff --git a/builder/test/qr.png b/builder/test/qr.png new file mode 100644 index 0000000000000000000000000000000000000000..4044d85d549952c5deec91a91697bed2dc1c314d GIT binary patch literal 1848 zcmZuy3s4hR6kUbNiiRw;ilUKhsP#k5id4anq>@AtM?NBHup$T=FbGKq!Gw<}8cei; z<5XmrLO`oUgA%`$e<(280wqPk4~VFN2tf)FlOaf;n`pI8*`0mw-uuow_uhB!n>`j3 z;ES<$wg&(Jc54wn-@=v z+{24v6UFg-3jiRCNvIgl7DnR5@p0Tll9)oUB1ougF(U-L6(Zb6A%yw|;psd98^4yg zinxkEwa4S}WWk` zOy&wB#VfgquJ1_xh2f_sB74gLa2Fr?7o^$LG)V0aR2K)x~6+ z!LyyPJ%%5ilL5V0>TH^Dq)BYCwltaDB1f8x|14uYs6XUN105a65C|{kGeEd5!)`j< zV3Q5P46ot!c!{{y5te{OyNfaTDg9@)q-3 zL8>!Ws|hI8b?+SNPir4dh841vx3TaUY*|M*xOGEZdG!H0KW1`Btp9~Sz%;v6|D@{t z7GZdvtJh>g^>&YTrh~0neZyiS%GF%pN6{-f3XL&otmw&Cx%wQ6;SEdA30#NRI+lky zz{I6MCl;3d-h2H3ec5=~{Wga^9Z|a@hlrH0myJ@GH2W~1Gr%W+KpCHm!njd%f<61@)5f@_2n_Fg` zg(|kV(8AX8+qs}co*e7&DGX3QNnSDUersXMT&1o%z8e>4Nd11QI^CYc9rgr7adTaI z)oCXJ4|cd!UyrF#Tz*|S7E75r2+`%rIsIx=ka}#dvL}|@;Zd^#t4V4~hASL#ixJ6u zYVF^z=`t=udiMrTrfUf?Hr$9+-ltgGLodrQ!l(Ew*`Es?^C|YD-^Ux^cKra6}?qZm(1a$6K%H3E|CegmiDn{L!~Ic zqjj@V$4t4yGPd0>9N&tL-8mz)GLSfeHVCFIeyN1-7^dZ+w}C#msl%~3? zEWGCt(JnVEKgcTlUo|I<3lEQmh>9u`OH9m~STi$~$-6~mtJ6-8(4bRIz0eE6^s8pb za+O?EaDb+%;L84GhDKGaa*R#wC`r3HbY_GmbS06)LHD#nUAg`FEQDAX(YXRz`tR9DX{5P{dY=fml+MA}Unf}*-MRUf5+SXI(fxS`@y o3MLa;oIX%RmnFFxoX70``0K~DU&`n2v;2qrHUuzAX}b>p12!-#yZ`_I literal 0 HcmV?d00001 diff --git a/builder/test/test_qr.py b/builder/test/test_qr.py new file mode 100755 index 00000000..99f0fd2f --- /dev/null +++ b/builder/test/test_qr.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +# Test QG recognition example +# Should be synced with the documentation: /docs/en/camera.md, /docs/ru/camera.md +# TODO: use real ROS topics + +import rospy +from pyzbar import pyzbar +from cv_bridge import CvBridge +from sensor_msgs.msg import Image + +bridge = CvBridge() + +# rospy.init_node('barcode_test') + +# Image subscriber callback function +def image_callback(data): + cv_image = bridge.imgmsg_to_cv2(data, 'bgr8') # OpenCV image + barcodes = pyzbar.decode(cv_image) + for barcode in barcodes: + b_data = barcode.data.decode("utf-8") + b_type = barcode.type + (x, y, w, h) = barcode.rect + xc = x + w/2 + yc = y + h/2 + print("Found {} with data {} with center at x={}, y={}".format(b_type, b_data, xc, yc)) + # rospy.signal_shutdown('done') + +# image_sub = rospy.Subscriber('main_camera/image_raw', Image, image_callback, queue_size=1) + +# ============================================================================== +# Publish test image +# rospy.sleep(2) + +import cv2 +img = cv2.imread('qr.png') +image_callback(bridge.cv2_to_imgmsg(img, 'bgr8')) + +# image_pub = rospy.Publisher('/main_camera/image_raw', Image, queue_size=1, latch=True) +# image_pub.publish(bridge.cv2_to_imgmsg(img, 'bgr8')) + +# rospy.spin() diff --git a/builder/test/tests.py b/builder/test/tests.py index 4fd42aa8..2af19852 100755 --- a/builder/test/tests.py +++ b/builder/test/tests.py @@ -1,21 +1,29 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # validate all required modules installed import rospy from geometry_msgs.msg import PoseStamped +from sensor_msgs.msg import Range, BatteryState import cv2 import cv2.aruco +from sensor_msgs.msg import Image +from cv_bridge import CvBridge import numpy import mavros -from mavros_msgs.msg import State, StatusText, ExtendedState +from mavros_msgs.msg import State, StatusText, ExtendedState, RCIn, Mavlink from mavros_msgs.srv import CommandBool, CommandLong, SetMode from std_srvs.srv import Trigger from clover.srv import GetTelemetry, Navigate, NavigateGlobal, SetPosition, SetVelocity, \ SetAttitude, SetRates, SetLEDEffect +from led_msgs.srv import SetLEDs +from led_msgs.msg import LEDStateArray, LEDState +from aruco_pose.msg import Marker, MarkerArray, Point2D + +import dynamic_reconfigure.client import tf2_ros import tf2_geometry_msgs @@ -28,4 +36,4 @@ import pigpio # from espeak import espeak from pyzbar import pyzbar -print cv2.getBuildInformation() +print(cv2.getBuildInformation()) diff --git a/builder/test/tests.sh b/builder/test/tests.sh index f4404346..a0c27418 100755 --- a/builder/test/tests.sh +++ b/builder/test/tests.sh @@ -54,6 +54,8 @@ rosversion usb_cam rosversion cv_camera rosversion web_video_server rosversion rosshow +rosversion nodelet +rosversion image_view # validate examples are present [[ $(ls /home/pi/examples/*) ]] diff --git a/builder/test/tests_clever.py b/builder/test/tests_clever.py index 8db8b6b0..ffe251cc 100755 --- a/builder/test/tests_clever.py +++ b/builder/test/tests_clever.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # test backwards compatibility diff --git a/clover/CMakeLists.txt b/clover/CMakeLists.txt index d779e5f7..2e96289e 100644 --- a/clover/CMakeLists.txt +++ b/clover/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8.3) +cmake_minimum_required(VERSION 3.0) project(clover) ## Compile as C++11, supported in ROS Kinetic and newer @@ -30,7 +30,15 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") find_package(GeographicLib REQUIRED) -find_package(OpenCV 3 REQUIRED +# Workaround for OpenCV 3/4 support +set(_opencv_version 4) +find_package(OpenCV ${_opencv_version} QUIET COMPONENTS calib3d imgproc) +if (NOT OpenCV_FOUND) + message(STATUS "Did not find OpenCV 4, searching for OpenCV 3") + set(_opencv_version 3) +endif() + +find_package(OpenCV ${_opencv_version} REQUIRED COMPONENTS calib3d imgproc @@ -254,6 +262,10 @@ target_link_libraries(${PROJECT_NAME} # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} # ) +catkin_install_python(PROGRAMS src/selfcheck.py + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + # Only install udev rules when building a Debian package # FIXME: Other operating systems may have other prefixes string(FIND ${CMAKE_INSTALL_PREFIX} "/opt/ros" _PREFIX_INDEX) diff --git a/clover/README.md b/clover/README.md index e3fd3147..51243812 100644 --- a/clover/README.md +++ b/clover/README.md @@ -4,7 +4,7 @@ A bundle for autonomous navigation and drone control. ## Manual installation -Install ROS Melodic according to the [documentation](http://wiki.ros.org/melodic/Installation), then [create a Catkin workspace](http://wiki.ros.org/catkin/Tutorials/create_a_workspace). +Install ROS Noetic according to the [documentation](http://wiki.ros.org/noetic/Installation), then [create a Catkin workspace](http://wiki.ros.org/catkin/Tutorials/create_a_workspace). Clone this repo to directory `~/catkin_ws/src/clover`: diff --git a/clover/launch/aruco.launch b/clover/launch/aruco.launch index c4ce5781..dfd69899 100644 --- a/clover/launch/aruco.launch +++ b/clover/launch/aruco.launch @@ -2,30 +2,37 @@ + + + - + - - + + + + + - + - - + + + diff --git a/clover/launch/clover.launch b/clover/launch/clover.launch index 870d6e18..26ec4454 100644 --- a/clover/launch/clover.launch +++ b/clover/launch/clover.launch @@ -36,18 +36,13 @@ - + - - - - - diff --git a/clover/launch/main_camera.launch b/clover/launch/main_camera.launch index aa52f04d..312a0f15 100644 --- a/clover/launch/main_camera.launch +++ b/clover/launch/main_camera.launch @@ -3,6 +3,7 @@ + @@ -17,9 +18,14 @@ + + + + + - - + + diff --git a/clover/package.xml b/clover/package.xml index 43842cb9..a4f5878b 100644 --- a/clover/package.xml +++ b/clover/package.xml @@ -1,5 +1,5 @@ - + clover 0.21.1 The Clover package @@ -37,7 +37,8 @@ rosbridge_server web_video_server tf2_web_republisher - python-lxml + python-lxml + python3-lxml python-pymavlink diff --git a/clover/src/selfcheck.py b/clover/src/selfcheck.py index 1a3cddf1..25e413e1 100755 --- a/clover/src/selfcheck.py +++ b/clover/src/selfcheck.py @@ -138,7 +138,7 @@ def mavlink_exec(cmd, timeout=3.0): timeout=3, baudrate=0, count=len(cmd), - data=map(ord, cmd.ljust(70, '\0'))) + data=[ord(c) for c in cmd.ljust(70, '\0')]) msg.pack(link) ros_msg = mavlink.convert_to_rosmsg(msg) mavlink_pub.publish(ros_msg) @@ -609,7 +609,7 @@ def check_rangefinder(): @check('Boot duration') def check_boot_duration(): - output = subprocess.check_output('systemd-analyze') + output = subprocess.check_output('systemd-analyze').decode() r = re.compile(r'([\d\.]+)s\s*$', flags=re.MULTILINE) duration = float(r.search(output).groups()[0]) if duration > 15: @@ -620,7 +620,7 @@ def check_boot_duration(): def check_cpu_usage(): WHITELIST = 'nodelet', CMD = "top -n 1 -b -i | tail -n +8 | awk '{ printf(\"%-8s\\t%-8s\\t%-8s\\n\", $1, $9, $12); }'" - output = subprocess.check_output(CMD, shell=True) + output = subprocess.check_output(CMD, shell=True).decode() processes = output.split('\n') for process in processes: if not process: @@ -636,7 +636,7 @@ def check_cpu_usage(): def check_clover_service(): try: output = subprocess.check_output('systemctl show -p ActiveState --value clover.service'.split(), - stderr=subprocess.STDOUT) + stderr=subprocess.STDOUT).decode() except subprocess.CalledProcessError as e: failure('systemctl returned %s: %s', e.returncode, e.output) return @@ -751,7 +751,7 @@ def check_rpi_health(): # = # In case of `get_throttled`, is a hexadecimal number # with some of the FLAGs OR'ed together - output = subprocess.check_output(['vcgencmd', 'get_throttled']) + output = subprocess.check_output(['vcgencmd', 'get_throttled']).decode() except OSError: failure('could not call vcgencmd binary; not a Raspberry Pi?') return diff --git a/clover/test/basic.py b/clover/test/basic.py index f9a767d8..283180ed 100755 --- a/clover/test/basic.py +++ b/clover/test/basic.py @@ -26,5 +26,10 @@ def test_simple_offboard_services_available(): rospy.wait_for_service('land', timeout=5) def test_web_video_server(node): - import urllib2 - urllib2.urlopen("http://localhost:8080").read() + try: + # Python 2 + import urllib2 as urllib + except ModuleNotFoundError: + # Python 3 + import urllib.request as urllib + urllib.urlopen("http://localhost:8080").read() diff --git a/clover_blocks/src/clover_blocks b/clover_blocks/src/clover_blocks index 82a12854..4000df8e 100755 --- a/clover_blocks/src/clover_blocks +++ b/clover_blocks/src/clover_blocks @@ -112,7 +112,7 @@ def run(req): 'print': _print, 'raw_input': _input} try: - exec req.code in g + exec(req.code, g) except Stop: rospy.loginfo('Program forced to stop') except Exception as e: diff --git a/clover_blocks/www/python.js b/clover_blocks/www/python.js index b8ee9510..365dfccf 100644 --- a/clover_blocks/www/python.js +++ b/clover_blocks/www/python.js @@ -391,7 +391,7 @@ Blockly.Python.set_led = function(block) { if (/^'(.*)'$/.test(colorCode)) { // is simple string let color = parseColor(colorCode); - return `set_leds([LEDState(index=${index}, r=${color.r}, g=${color.g}, b=${color.b})])\n`; + return `set_leds([LEDState(index=int(${index}), r=${color.r}, g=${color.g}, b=${color.b})])\n`; // TODO: check for simple int } else { let parseColor = Blockly.Python.provideFunction_('parse_color', [PARSE_COLOR]); return `set_leds([LEDState(index=${index}, **${parseColor}(${colorCode}))])\n`; diff --git a/clover_simulation/src/clover_simulation/__init__.py b/clover_simulation/src/clover_simulation/__init__.py index 30ed091d..72822c53 100644 --- a/clover_simulation/src/clover_simulation/__init__.py +++ b/clover_simulation/src/clover_simulation/__init__.py @@ -88,6 +88,6 @@ def aruco_gen(): off_x + marker.x, off_y + marker.y, off_z + marker.z, marker.roll, marker.pitch, marker.yaw) - output = open(source_world, 'w') if inplace else stdout + output = open(source_world, 'wb') if inplace else stdout save_world(world_tree, output) diff --git a/docs/assets/noetic.png b/docs/assets/noetic.png new file mode 100644 index 0000000000000000000000000000000000000000..ae4e377ae4232483a60b9907f302dc458198159a GIT binary patch literal 49073 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 B0DAxc literal 0 HcmV?d00001 diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index 91b9feb3..9acb42b3 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -91,7 +91,9 @@ * [Soldering safety](tb.md) * [LED strip (legacy)](leds_old.md) * [Contribution Guidelines](contributing.md) + * [COEX packages repository](packages.md) * [Migration to v0.20](migrate20.md) + * [Migration to v0.22](migrate22.md) * [Events](events.md) * [CopterHack-2022](copterhack2022.md) * [CopterHack-2021](copterhack2021.md) diff --git a/docs/en/aruco_map.md b/docs/en/aruco_map.md index 7867d772..5d6a5ff4 100644 --- a/docs/en/aruco_map.md +++ b/docs/en/aruco_map.md @@ -1,5 +1,9 @@ # Map-based navigation with ArUco markers +> **Note** The following applies to [image versions](image.md) **0.22** and up. Older documentation is still available for [for version **0.20**](https://github.com/CopterExpress/clover/blob/v0.20/docs/en/aruco_map.md). + + + > **Info** Marker detection requires the camera module to be correctly plugged in and [configured](camera_setup.md). @@ -39,13 +43,14 @@ marker_id marker_size x y z z_angle y_angle x_angle `N_angle` is the angle of rotation along the `N` axis in radians. -Map path is defined in the `map` parameter: +Файлы карт располагаются в каталоге `~/catkin_ws/src/clover/aruco_pose/map`. Название файла с картой задается в аргументе `map`: +Map files are located at the `~/catkin_ws/src/clover/aruco_pose/map` directory. Map file name is defined in the `map` argument: ```xml - + ``` -Some map examples are provided in [`~/catkin_ws/src/clover/aruco_pose/map`](https://github.com/CopterExpress/clover/tree/master/aruco_pose/map). +Some map examples are provided in [the directory](https://github.com/CopterExpress/clover/tree/master/aruco_pose/map). Grid maps may be generated using the `genmap.py` script: @@ -152,10 +157,10 @@ If the drone's altitude is not stable, try increasing the `MPC_Z_VEL_P` paramete In order to navigate using markers on the ceiling, mount the onboard camera so that it points up and [adjust the camera frame accordingly](camera_setup.md). -You should also set the `known_tilt` parameter to `map_flipped` in both `aruco_detect` and `aruco_map` sections of `~/catkin_ws/src/clover/clover/launch/aruco.launch`: +You should also set the `placement` parameter to `ceilin` in `~/catkin_ws/src/clover/clover/launch/aruco.launch`: ```xml - + ``` This will flip the `aruco_map` frame (making its **z** axis point downward). Thus, in order to fly 2 metres below ceiling, the `z` argument for the `navigate` service should be set to 2: diff --git a/docs/en/aruco_marker.md b/docs/en/aruco_marker.md index c75568a4..f2208f25 100644 --- a/docs/en/aruco_marker.md +++ b/docs/en/aruco_marker.md @@ -1,5 +1,9 @@ # ArUco marker detection +> **Note** The following applies to [image versions](image.md) **0.22** and up. Older documentation is still available for [for version **0.20**](https://github.com/CopterExpress/clover/blob/v0.20/docs/en/aruco_marker.md). + + + > **Info** Marker detection requires the camera module to be correctly plugged in and [configured](camera.md). `aruco_detect` module detects ArUco markers and publishes their positions in ROS topics and as [TF frames](frames.md). @@ -22,22 +26,20 @@ For enabling detection set the `aruco_detect` argument in `~/catkin_ws/src/clove ``` -For the module to work correctly the following parameters should be set: +For the module to work correctly the following arguments should also be set: ```xml - - - - + + ``` -`known_tilt` should be set to: +`placement` argument should be set to: -* `map` if *all* markers are on the ground; -* `map_flipped` if *all* markers are on the ceiling; +* `floor` if *all* markers are on the ground; +* `ceiling` if *all* markers are on the ceiling; * an empty string otherwise. -You may specify length for each marker individually by using the `length_override` parameter: +You may specify length for each marker individually by using the `length_override` parameter of the node `aruco_detect`: ```xml @@ -98,9 +100,9 @@ rospy.init_node('my_node') # ... def markers_callback(msg): - print 'Detected markers:': + print('Detected markers:'): for marker in msg.markers: - print 'Marker: %s' % marker + print('Marker: %s' % marker) # Create a Subscription object. Each time a message is posted in aruco_detect/markers, the markers_callback function is called with this message as its argument. rospy.Subscriber('aruco_detect/markers', MarkerArray, markers_callback) diff --git a/docs/en/auto_setup.md b/docs/en/auto_setup.md index 17213516..580ec75e 100644 --- a/docs/en/auto_setup.md +++ b/docs/en/auto_setup.md @@ -126,7 +126,7 @@ Ctrl+C Start a program `myprogram.py` using Python: ```bash -python myprogram.py +python3 myprogram.py ``` Journal of the events related to `clover` package. Scroll the list by pressing Enter or Ctrl+V (scrolls faster): @@ -411,7 +411,7 @@ The easiest way to send the program is to copy the content of the program, creat - Run the program: ```bash - python my_program.py + python3 my_program.py ``` > **Warning** After completion of the program , the drone can land incorrectly and continue to fly over the floor. In this case, you need to intercept control. diff --git a/docs/en/camera.md b/docs/en/camera.md index 0308b6ed..5dfe34fa 100644 --- a/docs/en/camera.md +++ b/docs/en/camera.md @@ -133,12 +133,12 @@ def image_callback(data): cv_image = bridge.imgmsg_to_cv2(data, 'bgr8') # OpenCV image barcodes = pyzbar.decode(cv_image) for barcode in barcodes: - b_data = barcode.data.encode("utf-8") + b_data = barcode.data.decode("utf-8") b_type = barcode.type (x, y, w, h) = barcode.rect xc = x + w/2 yc = y + h/2 - print ("Found {} with data {} with center at x={}, y={}".format(b_type, b_data, xc, yc)) + print("Found {} with data {} with center at x={}, y={}".format(b_type, b_data, xc, yc)) image_sub = rospy.Subscriber('main_camera/image_raw', Image, image_callback, queue_size=1) @@ -153,3 +153,13 @@ The script will take up to 100% CPU capacity. To slow down the script artificial ``` The topic for the subscriber in this case should be changed for `main_camera/image_raw_throttled`. + +## Video recording + +To record a video you can use [`video_recorder`](http://wiki.ros.org/image_view#image_view.2Fdiamondback.video_recorder) node from `image_view` package: + +```bash +rosrun image_view video_recorder image:=/main_camera/image_raw +``` + +The video file will be saved to a file `output.avi`. The `image` argument contains the name of the topic to record. diff --git a/docs/en/cli.md b/docs/en/cli.md index 6f634f10..3db1fc1e 100644 --- a/docs/en/cli.md +++ b/docs/en/cli.md @@ -39,7 +39,7 @@ cat file.py Run `file.py` as a Python script: ```bash -python file.py +python3 file.py ``` Reboot Raspberry Pi: diff --git a/docs/en/contributing.md b/docs/en/contributing.md index 5f56437e..d25f3124 100644 --- a/docs/en/contributing.md +++ b/docs/en/contributing.md @@ -96,3 +96,7 @@ Prepare your article and send it as a pull request to the [Clover repository](ht ## Easy way If the above instructions are too difficult for you, send your fixes and new articles by e-mail (okalachev@gmail.com) or in Telegram messenger (user @okalachev). + +## Publishing packages + +You also can publish a package, that extends Clover functionality, into the official [COEX Debian repository](packages.md). diff --git a/docs/en/image.md b/docs/en/image.md index ee81e447..454d60be 100644 --- a/docs/en/image.md +++ b/docs/en/image.md @@ -4,6 +4,8 @@ The RPi image for Clover contains all the necessary software for working with Cl ## Usage +> **Info** Starting from version v0.22, the image is based on ROS Noetic and using Python 3. If you want to use ROS Melodic and Python 2, use version [v0.21.2](https://github.com/CopterExpress/clover/releases/download/v0.21.2/clover_v0.21.2.img.zip). + 1. Download the latest stable release of the image – **download**. 2. Download and install [Etcher](https://www.balena.io/etcher/), the software for flashing images (available for Windows/Linux/macOS). 3. Put the MicroSD-card into your computer (use an adapter if necessary). diff --git a/docs/en/laser.md b/docs/en/laser.md index 283fd0c1..59dd1cb6 100644 --- a/docs/en/laser.md +++ b/docs/en/laser.md @@ -59,7 +59,7 @@ rospy.init_node('flight') def range_callback(msg): # Process data from the rangefinder - print 'Rangefinder distance:', msg.range + print('Rangefinder distance:', msg.range) rospy.Subscriber('rangefinder/range', Range, range_callback) diff --git a/docs/en/migrate20.md b/docs/en/migrate20.md index 8e92f050..2da6aed9 100644 --- a/docs/en/migrate20.md +++ b/docs/en/migrate20.md @@ -70,56 +70,6 @@ The `~/catkin_ws/src/clever/` directory is renamed to `~/catkin_ws/src/clover`. For example, `~/catkin_ws/src/clever/clever/launch/clever.launch` file is now `~/catkin_ws/src/clover/clover/launch/clover.launch`. - - ## Wi-Fi network configuration Wi-Fi networks' SSID is changed to `clover-XXXX` (where X is a random number), password is changed to `cloverwifi`. diff --git a/docs/en/migrate22.md b/docs/en/migrate22.md new file mode 100644 index 00000000..623dcb52 --- /dev/null +++ b/docs/en/migrate22.md @@ -0,0 +1,59 @@ +# Migration to version 0.22 + +## Python 3 transition + +Python 2 is [deprecated](https://www.python.org/doc/sunset-python-2/) since January 1st, 2020. The Clover platform moves to Python 3. + +For running flight script instead of `python` command: + +```bash +python flight.py +``` + +use `python3` command: + +```bash +python3 flight.py +``` + +Python 3 has certain syntax differences in comparison with the old version. Instead of `print` *operator*: + +```python +print 'Clover is the best' # this won't work +``` + +use `print` *function*: + +```python +print('Clover is the best') +``` + +The division operator operates floating points by default (instead of integer). Python 2: + +```python +>>> 10 / 4 +2 +``` + +Python 3: + +```python +>>> 10 / 4 +2.5 +``` + +For strings `unicode` type is used by default (instead of `str` type). + +Encoding specification (`# coding: utf8`) is not necessary any more. + +More details on all the language changes see in [appropriate article](https://sebastianraschka.com/Articles/2014_python_2_3_key_diff.html). + +## Move to ROS Noetic + + + +ROS Melodic version was updated to ROS Noetic. See the full list of changes in the [ROS official documentation](http://wiki.ros.org/noetic/Migration). + +## Changes in launch-files + +Configuration of ArUco-markers navigation is simplified. See details in [markers navigation](aruco_marker.md) and [markers map navigation](aruco_map.md) articles. diff --git a/docs/en/packages.md b/docs/en/packages.md new file mode 100644 index 00000000..0e85bffb --- /dev/null +++ b/docs/en/packages.md @@ -0,0 +1,27 @@ +# COEX packages repository + +COEX provides an open [Debian-repository](https://wiki.debian.org/DebianRepository) with ROS Noetic related prebuilt binary pacakges for `armhf` architecture. + +> **Info** Repository URL: http://packages.coex.tech. + +The repository is already addedd in [RPi image](image.md) and may be used for simple installation of additional ROS packages. + +## Packages publishing + +You can make a Pull Request in a git repository with packages, adding or updating your package (a file with `.deb` extension), that relates to Clover or ROS. After merging your package will be available for installation with `apt` utility: + +```bash +sudo apt install ros-noetic-clover-some-feature +``` + +Packages, that extend Clover functionality are recommended to be named with `clover_` prefix, e. g. `clover_some_feature`. + +## Using on a normal Raspberry Pi OS + +On a normal Raspberry Pi OS, the repository may be added to the sources list, this way: + +```bash +wget -O - 'http://packages.coex.tech/key.asc' | apt-key add - +echo 'deb http://packages.coex.tech buster main' >> /etc/apt/sources.list +sudo apt update +``` diff --git a/docs/en/programming.md b/docs/en/programming.md index 78f5ae34..fd72298b 100644 --- a/docs/en/programming.md +++ b/docs/en/programming.md @@ -42,10 +42,10 @@ Before the first flight it's recommended to check the Clover's configuration wit rosrun clover selfcheck.py ``` -In order to run a Python script use the `python` command: +In order to run a Python script use the `python3` command: ```bash -python flight.py +python3 flight.py ``` Below is a complete flight program that performs a takeoff, flies forward and lands: diff --git a/docs/en/ros.md b/docs/en/ros.md index 6753a2dc..ecc1a3e9 100644 --- a/docs/en/ros.md +++ b/docs/en/ros.md @@ -63,7 +63,7 @@ An example of subscription to topic `/foo`: ```python def foo_callback(msg): - print msg.data + print(msg.data) # Subscribing. When a message is received in topic /foo, function foo_callback will be invoked. rospy.Subscriber('/foo', String, foo_callback) diff --git a/docs/en/simple_offboard.md b/docs/en/simple_offboard.md index 05c742a1..d193f283 100644 --- a/docs/en/simple_offboard.md +++ b/docs/en/simple_offboard.md @@ -75,14 +75,14 @@ Displaying drone coordinates `x`, `y` and `z` in the local system of coordinates ```python telemetry = get_telemetry() -print telemetry.x, telemetry.y, telemetry.z +print(telemetry.x, telemetry.y, telemetry.z) ``` Displaying drone altitude relative to [the ArUco map](aruco.md): ```python telemetry = get_telemetry(frame_id='aruco_map') -print telemetry.z +print(telemetry.z) ``` Checking global position availability: @@ -90,9 +90,9 @@ Checking global position availability: ```python import math if not math.isnan(get_telemetry().lat): - print 'Global position is available' + print('Global position is available') else: - print 'No global position' + print('No global position') ``` Output of current telemetry (command line): @@ -303,7 +303,7 @@ Landing the drone: res = land() if res.success: - print 'drone is landing' + print('drone is landing') ``` Landing the drone (command line): diff --git a/docs/en/snippets.md b/docs/en/snippets.md index b8fe6539..6c8d2127 100644 --- a/docs/en/snippets.md +++ b/docs/en/snippets.md @@ -319,7 +319,7 @@ def flip(): rospy.loginfo('finish flip') set_position(x=start.x, y=start.y, z=start.z, yaw=start.yaw) # finish flip -print navigate(z=2, speed=1, frame_id='body', auto_arm=True) # take off +print(navigate(z=2, speed=1, frame_id='body', auto_arm=True)) # take off rospy.sleep(10) rospy.loginfo('flip') diff --git a/docs/en/sonar.md b/docs/en/sonar.md index f72bbfb4..95ab5fb9 100644 --- a/docs/en/sonar.md +++ b/docs/en/sonar.md @@ -83,7 +83,7 @@ pi.callback(ECHO, pigpio.FALLING_EDGE, fall) while True: # Reading the distance: - print read_distance() + print(read_distance()) ``` @@ -104,7 +104,7 @@ def read_distance_filtered(): return numpy.median(history) while True: - print read_distance_filtered() + print(read_distance_filtered()) ``` An example of charts of initial and filtered data: diff --git a/docs/ru/SUMMARY.md b/docs/ru/SUMMARY.md index 5f5e655d..b6f4ee2e 100644 --- a/docs/ru/SUMMARY.md +++ b/docs/ru/SUMMARY.md @@ -97,7 +97,9 @@ * [Подключение регулятора 4 в 1](4in1.md) * [Светодиодная лента (legacy)](leds_old.md) * [Вклад в Клевер](contributing.md) + * [Репозиторий пакетов COEX](packages.md) * [Переход на версию 0.20](migrate20.md) + * [Переход на версию 0.22](migrate22.md) * [COEX DuoCam](duocam.md) * [Виртуальная MAVLink-камера](duocam_mavlink.md) * [Мероприятия](events.md) diff --git a/docs/ru/aruco_map.md b/docs/ru/aruco_map.md index 8ef44ace..e34fa73f 100644 --- a/docs/ru/aruco_map.md +++ b/docs/ru/aruco_map.md @@ -1,5 +1,9 @@ # Навигация по картам ArUco-маркеров +> **Note** Документация для версий [образа](image.md), начиная с версии **0.22**. Для более ранних версий см. [документацию для версии **0.20**](https://github.com/CopterExpress/clover/blob/v0.20/docs/ru/aruco_map.md). + + + > **Info** Для распознавания маркеров модуль камеры должен быть корректно подключен и [сконфигурирован](camera.md). @@ -39,13 +43,13 @@ id_маркера размер_маркера x y z угол_z угол_y уго Где `угол_N` – это угол поворота маркера вокруг оси N в радианах. -Путь к файлу с картой задается в параметре `map`: +Файлы карт располагаются в каталоге `~/catkin_ws/src/clover/aruco_pose/map`. Название файла с картой задается в аргументе `map`: ```xml - + ``` -Смотрите примеры карт маркеров в каталоге [`~/catkin_ws/src/clover/aruco_pose/map`](https://github.com/CopterExpress/clover/tree/master/aruco_pose/map). +Смотрите примеры карт маркеров в [`вышеуказанном каталоге`](https://github.com/CopterExpress/clover/tree/master/aruco_pose/map). Файл карты может быть сгенерирован с помощью инструмента `genmap.py`: @@ -154,10 +158,10 @@ navigate(frame_id='aruco_5', x=0, y=0, z=1) Для навигации по маркерам, расположенным на потолке, необходимо поставить основную камеру так, чтобы она смотрела вверх и [установить соответствующий фрейм камеры](camera_setup.md#frame). -Также в файле `~/catkin_ws/src/clover/clover/launch/aruco.launch` необходимо установить параметр `known_tilt` в секциях `aruco_detect` и `aruco_map` в значение `map_flipped`: +Также в файле `~/catkin_ws/src/clover/clover/launch/aruco.launch` необходимо выставить аргумент `placement` в значение `ceiling`: ```xml - + ``` При такой конфигурации фрейм `aruco_map` также окажется перевернутым. Таким образом, для полета на высоту 2 метра ниже потолка, аргумент `z` нужно устанавливать в 2: diff --git a/docs/ru/aruco_marker.md b/docs/ru/aruco_marker.md index fee1ffe5..d7599544 100644 --- a/docs/ru/aruco_marker.md +++ b/docs/ru/aruco_marker.md @@ -1,5 +1,9 @@ # Распознавание ArUco-маркеров +> **Note** Документация для версий [образа](image.md), начиная с версии **0.22**. Для более ранних версий см. [документацию для версии **0.20**](https://github.com/CopterExpress/clover/blob/v0.20/docs/ru/aruco_marker.md). + + + > **Info** Для распознавания маркеров модуль камеры должен быть корректно подключен и [сконфигурирован](camera_setup.md). Модуль `aruco_detect` распознает ArUco-маркеры и публикует их позиции в ROS-топики и в [TF](frames.md). @@ -22,22 +26,20 @@ ``` -Для правильной работы в этом же файле в секции `aruco_detect` должны быть выставлены параметры: +Для правильной работы в этом же файле также должны быть выставлены аргументы: ```xml - - - - + + ``` -Значение параметра `known_tilt` следует выставлять следующим образом: +Значение аргумента `placement` следует выставлять следующим образом: -* если *все* маркеры наклеены на полу (земле), выставить значение `map`; -* если *все* маркеры наклеены на потолке, выставить значение `map_flipped`; +* если *все* маркеры наклеены на полу (земле), выставить значение `floor`; +* если *все* маркеры наклеены на потолке, выставить значение `ceiling`; * в противном случае удалить строку с параметром. -Если некоторые маркеры имеют размер, отличный значения `length`, их размер может быть переопределен с помощью параметра `length_override`: +Если некоторые маркеры имеют размер, отличный значения `length`, их размер может быть переопределен с помощью параметра `length_override` ноды `aruco_detect`: ```xml @@ -110,9 +112,9 @@ rospy.init_node('my_node') # ... def markers_callback(msg): - print 'Detected markers:': + print('Detected markers:'): for marker in msg.markers: - print 'Marker: %s' % marker + print('Marker: %s' % marker) # Подписываемся. При получении сообщения в топик aruco_detect/markers будет вызвана функция markers_callback. rospy.Subscriber('aruco_detect/markers', MarkerArray, markers_callback) diff --git a/docs/ru/auto_setup.md b/docs/ru/auto_setup.md index 594d0e40..81a5af58 100644 --- a/docs/ru/auto_setup.md +++ b/docs/ru/auto_setup.md @@ -126,7 +126,7 @@ Ctrl+C Запустить программу myprogram.py на Питоне: ```bash -python myprogram.py +python3 myprogram.py ``` Журнал событий процессов Клевера. Пролистывать список можно нажатием Enter или сочетанием клавиш Ctrl+V (пролистывает быстрее): @@ -406,7 +406,7 @@ sudo nano /etc/sudoers - Запустите программу. Для этого выполните команду: ```bash - python my_program.py + python3 my_program.py ``` > **Warning** После выполнения программы дрон может некорректно приземлиться и продолжать лететь над полом. В таком случае нужно перехватить управление. diff --git a/docs/ru/camera.md b/docs/ru/camera.md index 08ca25e9..522f89ba 100644 --- a/docs/ru/camera.md +++ b/docs/ru/camera.md @@ -135,12 +135,12 @@ def image_callback(data): cv_image = bridge.imgmsg_to_cv2(data, 'bgr8') # OpenCV image barcodes = pyzbar.decode(cv_image) for barcode in barcodes: - b_data = barcode.data.encode("utf-8") + b_data = barcode.data.decode("utf-8") b_type = barcode.type (x, y, w, h) = barcode.rect xc = x + w/2 yc = y + h/2 - print ("Found {} with data {} with center at x={}, y={}".format(b_type, b_data, xc, yc)) + print("Found {} with data {} with center at x={}, y={}".format(b_type, b_data, xc, yc)) image_sub = rospy.Subscriber('main_camera/image_raw', Image, image_callback, queue_size=1) @@ -155,3 +155,13 @@ rospy.spin() ``` Топик для подписчика в этом случае необходимо поменять на `main_camera/image_raw_throttled`. + +## Запись видео + +Для записи видео может использована нода [`video_recorder`](http://wiki.ros.org/image_view#image_view.2Fdiamondback.video_recorder) из пакета `image_view`: + +```bash +rosrun image_view video_recorder image:=/main_camera/image_raw +``` + +Видео будет сохранено в файл `output.avi`. В аргументе `image` указывается название топика для записи видео. diff --git a/docs/ru/cli.md b/docs/ru/cli.md index d8c80196..35ed33df 100644 --- a/docs/ru/cli.md +++ b/docs/ru/cli.md @@ -39,7 +39,7 @@ cat file.py Запустить Python-скрипт `file.py`: ```bash -python file.py +python3 file.py ``` Перезагрузить Raspberry Pi: diff --git a/docs/ru/contributing.md b/docs/ru/contributing.md index a183b74e..8b8657f4 100644 --- a/docs/ru/contributing.md +++ b/docs/ru/contributing.md @@ -96,3 +96,7 @@ ## Простой способ Если вышеприведенные инструкции для вас оказываются слишком сложными, отправляйте правки или новые статьи по e-mail (okalachev@gmail.com) или в Telegram (пользователь @okalachev). + +## Публикация пакетов + +Вы также можете опубликовать собственный пакет, расширяющий функциональность Клевера, в [Debian-репозитории COEX](packages.md). diff --git a/docs/ru/image.md b/docs/ru/image.md index 072781ea..4956e586 100644 --- a/docs/ru/image.md +++ b/docs/ru/image.md @@ -4,6 +4,8 @@ ## Использование +> **Info** Начиная с версии v0.22, образ основан на ROS Noetic и использует Python 3. Если вы хотите использовать ROS Melodic и Python 2, используйте версию [v0.21.2](https://github.com/CopterExpress/clover/releases/download/v0.21.2/clover_v0.21.2.img.zip). + 1. Скачайте последний стабильный релиз образа — **скачать**. 2. Скачайте и установите [программу для записи образов Etcher](https://www.balena.io/etcher/) (доступна для Windows/Linux/macOS). 3. Установите MicroSD-карту в компьютер (используйте адаптер при необходимости). diff --git a/docs/ru/laser.md b/docs/ru/laser.md index 7b9c7623..ba54374e 100644 --- a/docs/ru/laser.md +++ b/docs/ru/laser.md @@ -59,7 +59,7 @@ rospy.init_node('flight') def range_callback(msg): # Обработка новых данных с дальномера - print 'Rangefinder distance:', msg.range + print('Rangefinder distance:', msg.range) rospy.Subscriber('rangefinder/range', Range, range_callback) diff --git a/docs/ru/migrate20.md b/docs/ru/migrate20.md index 2adf2624..09917e40 100644 --- a/docs/ru/migrate20.md +++ b/docs/ru/migrate20.md @@ -72,56 +72,6 @@ sudo systemctl restart clover Например, файл `~/catkin_ws/src/clever/clever/launch/clever.launch` теперь называется `~/catkin_ws/src/clover/clover/launch/clover.launch`. - - ## Настройки Wi-Fi сети SSID Wi-Fi сети изменен на `clover-XXXX` (где X – случайная цифра), пароль изменен на `cloverwifi`. diff --git a/docs/ru/migrate22.md b/docs/ru/migrate22.md new file mode 100644 index 00000000..780c2121 --- /dev/null +++ b/docs/ru/migrate22.md @@ -0,0 +1,59 @@ +# Переход на версию 0.22 + +## Переход на Python 3 + +Python 2 был признан [устаревшим](https://www.python.org/doc/sunset-python-2/), начиная с 1 января 2020 года. Платформа Клевера переходит на использование Python 3. + +Для запуска полетных скриптов вместо команды `python`: + +```bash +python flight.py +``` + +теперь следует использовать команду `python3`: + +```bash +python3 flight.py +``` + +Синтаксис языка Python 3 имеет определенные изменения по сравнения со второй версией. Вместо *оператора* `print`: + +```python +print 'Clover is the best' # this won't work +``` + +теперь используется *функция* `print`: + +```python +print('Clover is the best') +``` + +Оператор деления по умолчанию выполняет деление с плавающей точкой (вместо целочисленного). Python 2: + +```python +>>> 10 / 4 +2 +``` + +Python 3: + +```python +>>> 10 / 4 +2.5 +``` + +Для строк по умолчанию теперь используется тип `unicode` (вместо типа `str`). + +Указание кодировки файла (`# coding: utf8`) перестало быть необходимым. + +Полное описание всех изменений языка смотрите в [соответствующей статье](https://pythonworld.ru/osnovy/python2-vs-python3-razlichiya-sintaksisa.html). + +## Переход на ROS Noetic + + + +Версия ROS Melodic обновлена до ROS Noetic. Смотрите полный список изменений в [официальной документации ROS](http://wiki.ros.org/noetic/Migration). + +## Изменения в launch-файлах + +Упрощено конфигурирование навигации с использованием ArUco-маркеров. Подробнее в статьях по [навигации по маркерам](aruco_marker.md) и [навигации по картам маркеров](aruco_map.md). diff --git a/docs/ru/packages.md b/docs/ru/packages.md new file mode 100644 index 00000000..935b9568 --- /dev/null +++ b/docs/ru/packages.md @@ -0,0 +1,27 @@ +# Репозиторий пакетов COEX + +COEX предоставляет открытый [Debian-репозиторий](https://wiki.debian.org/ru/SourcesList) с предсобранными пакетами, относящимися к ROS Noetic, для архитектуры `armhf`. + +> **Info** Адрес репозитория: http://packages.coex.tech. + +Репозиторий подключен в [образе для RPi](image.md) и может быть использован для легкой установки дополнительных ROS-пакетов. + +## Публикация пакетов + +Вы можете прислать Pull Request в [git-репозиторий с пакетами](https://github.com/CopterExpress/packages), добавляющий или обновляющий ваш пакет (файл с расширением `.deb`), относящийся с Клеверу или ROS. После принятия ваш пакет будет доступен для установки с помощью утилиты `apt`: + +```bash +sudo apt install ros-noetic-clover-some-feature +``` + +Пакеты, расширяющие функциональность Клевера, рекомендуется называть с префиксом `clover_`, например `clover_some_feature`. + +## Использование на обычной Raspberry Pi OS + +На обычной Raspberry Pi OS репозиторий может быть добавлен в список источников пакетов следующими командами: + +```bash +wget -O - 'http://packages.coex.tech/key.asc' | apt-key add - +echo 'deb http://packages.coex.tech buster main' >> /etc/apt/sources.list +sudo apt update +``` diff --git a/docs/ru/programming.md b/docs/ru/programming.md index b4e5c395..ef8fe8e8 100644 --- a/docs/ru/programming.md +++ b/docs/ru/programming.md @@ -42,10 +42,10 @@ rosrun clover selfcheck.py ``` -Для того, чтобы запустить Python-скрипт, используйте команду `python`: +Для того, чтобы запустить Python-скрипт, используйте команду `python3`: ```bash -python flight.py +python3 flight.py ``` Пример программы для полета (взлет, пролет вперед, посадка): diff --git a/docs/ru/ros.md b/docs/ru/ros.md index 2704603e..ff11c2e8 100644 --- a/docs/ru/ros.md +++ b/docs/ru/ros.md @@ -63,7 +63,7 @@ foo_pub.publish(data='Hello, world!') # публикуем сообщение ```python def foo_callback(msg): - print msg.data + print(msg.data) # Подписываемся. При получении сообщения в топик /foo будет вызвана функция foo_callback. rospy.Subscriber('/foo', String, foo_callback) diff --git a/docs/ru/simple_offboard.md b/docs/ru/simple_offboard.md index a9225c86..ba8a860d 100644 --- a/docs/ru/simple_offboard.md +++ b/docs/ru/simple_offboard.md @@ -75,14 +75,14 @@ land = rospy.ServiceProxy('land', Trigger) ```python telemetry = get_telemetry() -print telemetry.x, telemetry.y, telemetry.z +print(telemetry.x, telemetry.y, telemetry.z) ``` Вывод высоты коптера относительно [карты ArUco-меток](aruco.md): ```python telemetry = get_telemetry(frame_id='aruco_map') -print telemetry.z +print(telemetry.z) ``` Проверка доступности глобальной позиции: @@ -90,9 +90,9 @@ print telemetry.z ```python import math if not math.isnan(get_telemetry().lat): - print 'Global position is available' + print('Global position is available') else: - print 'No global position' + print('No global position') ``` Вывод текущей телеметрии (командная строка): @@ -303,7 +303,7 @@ set_velocity(vx=1, vy=0.0, vz=0, frame_id='body') res = land() if res.success: - print 'Copter is landing' + print('Copter is landing') ``` Посадка коптера (командная строка): diff --git a/docs/ru/snippets.md b/docs/ru/snippets.md index af71488f..e7219bf7 100644 --- a/docs/ru/snippets.md +++ b/docs/ru/snippets.md @@ -337,7 +337,7 @@ def flip(): rospy.loginfo('finish flip') set_position(x=start.x, y=start.y, z=start.z, yaw=start.yaw) # finish flip -print navigate(z=2, speed=1, frame_id='body', auto_arm=True) # take off +print(navigate(z=2, speed=1, frame_id='body', auto_arm=True)) # take off rospy.sleep(10) rospy.loginfo('flip') diff --git a/docs/ru/sonar.md b/docs/ru/sonar.md index 042530f5..7625fad6 100644 --- a/docs/ru/sonar.md +++ b/docs/ru/sonar.md @@ -83,7 +83,7 @@ pi.callback(ECHO, pigpio.FALLING_EDGE, fall) while True: # Читаем дистанцию: - print read_distance() + print(read_distance()) ``` @@ -104,7 +104,7 @@ def read_distance_filtered(): return numpy.median(history) while True: - print read_distance_filtered() + print(read_distance_filtered()) ``` Пример графиков исходных и отфильтрованных данных: diff --git a/roswww_static/CMakeLists.txt b/roswww_static/CMakeLists.txt index c9334100..bb9e1fc7 100644 --- a/roswww_static/CMakeLists.txt +++ b/roswww_static/CMakeLists.txt @@ -6,3 +6,7 @@ find_package(catkin REQUIRED) catkin_package() install(DIRECTORY launch DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}) + +catkin_install_python(PROGRAMS main.py + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +)