From 2b5688994ff0b8452f962ddd18810100430073ef Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sat, 4 Jan 2025 13:21:15 -0600 Subject: [PATCH 01/14] Add point cloud calculation to SIMZEDCam. --- src/vision/cameras/sim/SIMZEDCam.cpp | 151 ++++++++++++++++----------- 1 file changed, 90 insertions(+), 61 deletions(-) diff --git a/src/vision/cameras/sim/SIMZEDCam.cpp b/src/vision/cameras/sim/SIMZEDCam.cpp index 314a7fbb..45380402 100644 --- a/src/vision/cameras/sim/SIMZEDCam.cpp +++ b/src/vision/cameras/sim/SIMZEDCam.cpp @@ -65,7 +65,7 @@ SIMZEDCam::SIMZEDCam(const std::string szCameraPath, // Initialize OpenCV mats to a black/empty image the size of the camera resolution. m_cvFrame = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_8UC4); m_cvDepthImage = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_8UC3); - m_cvDepthMeasure = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_16UC1); + m_cvDepthMeasure = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_32FC1); m_cvDepthBuffer = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_8UC3); m_cvPointCloud = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_32FC4); @@ -161,6 +161,94 @@ void SIMZEDCam::ThreadedContinuousCode() // Release lock. lkRoverPoseLock.unlock(); + // Acquire a lock on the WebRTC mutex. + std::shared_lock lkWebRTC3(m_muWebRTCDepthMeasureCopyMutex); + // The Simulator uses this a special method of packing the depth measure data as defined in this paper. + // http://reality.cs.ucl.ac.uk/projects/depth-streaming/depth-streaming.pdf + // Here we will decode it. + float fW = 65536.0; + float fNP = 512.0; + + // Iterate over each pixel in the cvDepthMeasure image + for (int nY = 0; nY < m_cvDepthBuffer.rows; ++nY) + { + for (int nX = 0; nX < m_cvDepthBuffer.cols; ++nX) + { + // Extract the encoded depth values + cv::Vec3b cvEncodedDepth = m_cvDepthBuffer.at(nY, nX); + + // Extract encoded values + float fL = cvEncodedDepth[2] / 255.0; + float fHa = cvEncodedDepth[1] / 255.0; + float fHb = cvEncodedDepth[0] / 255.0; + + // Period for triangle waves + float fP = fNP / fW; + + // Determine offset and fine-grain correction + int fM = fmod((4.0 * (fL / fP)) - 0.5, 4.0); + float fL0 = fL - fmod(fL - (fP / 8.0), fP) + ((fP / 4.0) * fM) - (fP / 8.0); + + float fDelta = 0.0f; + if (fM == 0) + fDelta = (fP / 2.0) * fHa; + else if (fM == 1) + fDelta = (fP / 2.0) * fHb; + else if (fM == 2) + fDelta = (fP / 2.0) * (1.0 - fHa); + else if (fM == 3) + fDelta = (fP / 2.0) * (1.0 - fHb); + + // Combine to compute the original depth + float fDepth = fW * (fL0 + fDelta); + + // Check if the depth is within the bounds of the depth image + if (fDepth < 0.0) + fDepth = 0.0; + else if (fDepth > 65535.0) + fDepth = 65535.0; + + // Check if nY and nX are within the bounds of the depth image + if (nY < m_cvDepthMeasure.rows && nX < m_cvDepthMeasure.cols) + { + // Store the decoded depth in the new cv::Mat. Convert cm to m. + m_cvDepthMeasure.at(nY, nX) = static_cast(fDepth / 100.0); + } + } + } + // Release lock. + lkWebRTC3.unlock(); + + // Use the decoded depth measure to create a point cloud. No need to lock since WebRTC only uses the depth buffer, and we will only read the measure. + for (int nY = 0; nY < m_cvDepthMeasure.rows; ++nY) + { + for (int nX = 0; nX < m_cvDepthMeasure.cols; ++nX) + { + // Get the depth value. + float fDepth = m_cvDepthMeasure.at(nY, nX); + + // Get the horizontal and vertical FOV. + double dHorizontalFOV = m_dPropHorizontalFOV; + double dVerticalFOV = m_dPropVerticalFOV; + + // Get the horizontal and vertical angles. + double dHorizontalAngle = (nX - m_cvDepthMeasure.cols / 2.0) * dHorizontalFOV / m_cvDepthMeasure.cols; + double dVerticalAngle = (nY - m_cvDepthMeasure.rows / 2.0) * dVerticalFOV / m_cvDepthMeasure.rows; + + // Convert angles to radians. + double dHorizontalAngleRad = dHorizontalAngle * M_PI / 180.0; + double dVerticalAngleRad = dVerticalAngle * M_PI / 180.0; + + // Calculate the Cartesian coordinates. + float fX = fDepth * sin(dHorizontalAngleRad); + float fY = fDepth * sin(dVerticalAngleRad); + float fZ = fDepth * cos(dHorizontalAngleRad) * cos(dVerticalAngleRad); + + // Store the decoded depth in the new cv::Mat + m_cvPointCloud.at(nY, nX) = cv::Vec4f(fX, fY, fZ, 1.0); + } + } + // Acquire a shared_lock on the frame copy queue. std::shared_lock lkSchedulers(m_muPoolScheduleMutex); // Check if the frame copy queue is empty. @@ -239,66 +327,7 @@ void SIMZEDCam::PooledLinearCode() { case PIXEL_FORMATS::eBGRA: *(stContainer.pFrame) = m_cvFrame.clone(); break; case PIXEL_FORMATS::eDepthImage: *(stContainer.pFrame) = m_cvDepthImage.clone(); break; - case PIXEL_FORMATS::eDepthMeasure: - { - // Copy depth buffer. - - // The Simulator uses this a special method of packing the depth measure data as defined in this paper. - // http://reality.cs.ucl.ac.uk/projects/depth-streaming/depth-streaming.pdf - // Here we will decode it. - float fW = 65536.0; - float fNP = 512.0; - - // Iterate over each pixel in the cvDepthMeasure image - for (int nY = 0; nY < m_cvDepthBuffer.rows; ++nY) - { - for (int nX = 0; nX < m_cvDepthBuffer.cols; ++nX) - { - // Extract the encoded depth values - cv::Vec3b cvEncodedDepth = m_cvDepthBuffer.at(nY, nX); - - // Extract encoded values - float fL = cvEncodedDepth[2] / 255.0; - float fHa = cvEncodedDepth[1] / 255.0; - float fHb = cvEncodedDepth[0] / 255.0; - - // Period for triangle waves - float fP = fNP / fW; - - // Determine offset and fine-grain correction - int fM = fmod((4.0 * (fL / fP)) - 0.5, 4.0); - float fL0 = fL - fmod(fL - (fP / 8.0), fP) + ((fP / 4.0) * fM) - (fP / 8.0); - - float fDelta = 0.0f; - if (fM == 0) - fDelta = (fP / 2.0) * fHa; - else if (fM == 1) - fDelta = (fP / 2.0) * fHb; - else if (fM == 2) - fDelta = (fP / 2.0) * (1.0 - fHa); - else if (fM == 3) - fDelta = (fP / 2.0) * (1.0 - fHb); - - // Combine to compute the original depth - float fDepth = fW * (fL0 + fDelta); - - // Check if the depth is within the bounds of the depth image - if (fDepth < 0.0) - fDepth = 0.0; - else if (fDepth > 65535.0) - fDepth = 65535.0; - - // Check if nY and nX are within the bounds of the depth image - if (nY < m_cvDepthMeasure.rows && nX < m_cvDepthMeasure.cols) - { - // Store the decoded depth in the new cv::Mat - m_cvDepthMeasure.at(nY, nX) = static_cast(fDepth); - } - } - } - *(stContainer.pFrame) = m_cvDepthMeasure.clone(); - break; - } + case PIXEL_FORMATS::eDepthMeasure: *(stContainer.pFrame) = m_cvDepthMeasure.clone(); break; case PIXEL_FORMATS::eXYZ: *(stContainer.pFrame) = m_cvPointCloud.clone(); break; default: *(stContainer.pFrame) = m_cvFrame.clone(); break; } From b4c37f41d57c4ca23bb5d384acac72c5694bff57 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sat, 4 Jan 2025 18:12:02 -0600 Subject: [PATCH 02/14] PR review changes. --- CMakeLists.txt | 2 +- .../Autonomy-Dictionary.txt | 64 +++++++++ examples/vision/cameras/OpenBasicCam.hpp | 2 +- examples/vision/dnn/InferenceYOLOModel.hpp | 2 +- .../tagdetection/ArucoDetectionBasicCam.hpp | 2 +- .../vision/tagdetection/ArucoDetectionZED.hpp | 4 +- src/handlers/WaypointHandler.cpp | 4 +- src/interfaces/BasicCamera.hpp | 2 +- src/interfaces/ZEDCamera.hpp | 2 +- src/states/SearchPatternState.cpp | 2 +- src/vision/aruco/TagDetector.cpp | 2 +- src/vision/cameras/sim/SIMZEDCam.cpp | 134 +++++++++++------- src/vision/cameras/sim/SIMZEDCam.h | 3 +- src/vision/cameras/sim/WebRTC.cpp | 20 ++- .../ffmpeg/ffmpeg-amd64-pkg.sh | 48 ++++++- .../libdatachannel-arm64-pkg.sh | 2 +- 16 files changed, 224 insertions(+), 71 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 82a83a5d..b0d16b98 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ cmake_policy(SET CMP0153 OLD) # Allows use of "Exec_Program" function #################################################################################################################### ## Enable or Disable Simulation Mode -option(BUILD_SIM_MODE "Enable Simulation Mode" ON) +option(BUILD_SIM_MODE "Enable Simulation Mode" OFF) ## Enable or Disable Code Coverage Mode option(BUILD_CODE_COVERAGE "Enable Code Coverage Mode" OFF) diff --git a/data/Custom_Dictionaries/Autonomy-Dictionary.txt b/data/Custom_Dictionaries/Autonomy-Dictionary.txt index 6513f5c8..1fa5f06d 100644 --- a/data/Custom_Dictionaries/Autonomy-Dictionary.txt +++ b/data/Custom_Dictionaries/Autonomy-Dictionary.txt @@ -41,6 +41,7 @@ CAFFE CALIB CELLVOLTAGE charconv +chromaprint chrono clayjay CLEARWAYPOINTS @@ -202,6 +203,7 @@ Geospatial gfortran gmock GNSS +gnutls GPSLATLONALT GPSMDRS GPSSDELC @@ -254,49 +256,106 @@ jthread kbits keyrings keyscan +ladspa latlng latlon ldconfig LEDRGB LEFTCAM +libaom +libass libatlas libavcodec libavdevice libavfilter libavformat libavutil +libbluray libboost +libbs libc +libcaca libcanberra +libcdio +libchromaprint +libcodec libdatachannel +libdav +libdc libdrm libedgetpu libeigen +libfdk +libflite +libfontconfig +libfreetype +libfribidi libglew +libglslang +libgme +libgnutls libgomp +libgsm libgstreamer libgtkglext +libharfbuzz +libiec +libjack libjpeg +libjxl liblapack liblapacke +libmfx +libmp +libmysofa libopenblas +libopenjpeg +libopenmpt +libopus +libplacebo libpostproc libpq +libpulse +librabbitmq +librav +librist +librsvg +librubberband +libshine +libsnappy +libsoxr +libspeex +libsrt +libssh +libsvtav libswresample libswscale libtbb libtesseract +libtheora +libtwolame libusb libv +libvidstab +libvorbis +libvpl +libvpx +libwebp libx libxine +libxvid libxvidcore +libzimg +libzmq +libzvbi localtime LOGNAME +lpthread m_muWebRTCRGBImageCopyMutex MAINCAM MAINCAN mainpage +Makefiles MAKEFLAGS MAPANGLE MAPRANGE @@ -321,6 +380,7 @@ NAVIGATIONBOARD navpvt nJter nlohmann +nonfree noninteractive nullptr numops @@ -330,9 +390,11 @@ OBJDETECTION OBJECTDETECT Objectness odometry +openal OPENCL opencontainers opencv +opengl OPENMP ostream OTSU @@ -347,6 +409,7 @@ pispcl PIXELTYPE PKNS PNSD +pocketsphinx POSETRACK Pranswer pthread @@ -393,6 +456,7 @@ SIMBASICCAM SIMCAM SIMZED SIMZEDCAM +sndio sockaddr softprops SPSC diff --git a/examples/vision/cameras/OpenBasicCam.hpp b/examples/vision/cameras/OpenBasicCam.hpp index cf7d2e7e..8b573a80 100644 --- a/examples/vision/cameras/OpenBasicCam.hpp +++ b/examples/vision/cameras/OpenBasicCam.hpp @@ -38,7 +38,7 @@ void RunExample() globals::g_pCameraHandler = new CameraHandler(); // Get reference to camera. - ZEDCamera* ExampleBasicCam1 = globals::g_pCameraHandler->GetBasicCam(CameraHandler::BasicCamName::eHeadGroundCam); + BasicCamera* ExampleBasicCam1 = globals::g_pCameraHandler->GetBasicCam(CameraHandler::BasicCamName::eHeadGroundCam); // Start basic cam. ExampleBasicCam1->Start(); diff --git a/examples/vision/dnn/InferenceYOLOModel.hpp b/examples/vision/dnn/InferenceYOLOModel.hpp index c849c7f0..89fa061a 100644 --- a/examples/vision/dnn/InferenceYOLOModel.hpp +++ b/examples/vision/dnn/InferenceYOLOModel.hpp @@ -37,7 +37,7 @@ void RunExample() globals::g_pCameraHandler = new CameraHandler(); // Get reference to camera. - ZEDCamera* ExampleBasicCam1 = globals::g_pCameraHandler->GetBasicCam(CameraHandler::eHeadGroundCam); + BasicCamera* ExampleBasicCam1 = globals::g_pCameraHandler->GetBasicCam(CameraHandler::BasicCamName::eHeadGroundCam); // Start basic cam. ExampleBasicCam1->Start(); diff --git a/examples/vision/tagdetection/ArucoDetectionBasicCam.hpp b/examples/vision/tagdetection/ArucoDetectionBasicCam.hpp index 68c2a475..b6b98594 100644 --- a/examples/vision/tagdetection/ArucoDetectionBasicCam.hpp +++ b/examples/vision/tagdetection/ArucoDetectionBasicCam.hpp @@ -29,7 +29,7 @@ void RunExample() globals::g_pTagDetectionHandler = new TagDetectionHandler(); // Get pointer to camera. - ZEDCamera* ExampleBasicCam1 = globals::g_pCameraHandler->GetBasicCam(CameraHandler::eHeadGroundCam); + BasicCamera* ExampleBasicCam1 = globals::g_pCameraHandler->GetBasicCam(CameraHandler::BasicCamName::eHeadGroundCam); // Start basic cam. ExampleBasicCam1->Start(); diff --git a/examples/vision/tagdetection/ArucoDetectionZED.hpp b/examples/vision/tagdetection/ArucoDetectionZED.hpp index abc3d5a1..2605c739 100644 --- a/examples/vision/tagdetection/ArucoDetectionZED.hpp +++ b/examples/vision/tagdetection/ArucoDetectionZED.hpp @@ -30,12 +30,12 @@ void RunExample() globals::g_pTagDetectionHandler = new TagDetectionHandler(); // Get pointer to camera. - ZEDCamera* ExampleZEDCam1 = globals::g_pCameraHandler->GetZED(CameraHandler::eHeadMainCam); + ZEDCamera* ExampleZEDCam1 = globals::g_pCameraHandler->GetZED(CameraHandler::ZEDCamName::eHeadMainCam); // Start basic cam. ExampleZEDCam1->Start(); // Get pointer to the tag detector for the basic cam. - TagDetector* ExampleTagDetector1 = globals::g_pTagDetectionHandler->GetTagDetector(TagDetectionHandler::eHeadMainCam); + TagDetector* ExampleTagDetector1 = globals::g_pTagDetectionHandler->GetTagDetector(TagDetectionHandler::TagDetectors::eHeadMainCam); // Start the basic cam detector. ExampleTagDetector1->Start(); diff --git a/src/handlers/WaypointHandler.cpp b/src/handlers/WaypointHandler.cpp index 1def4bcc..416ef0f2 100755 --- a/src/handlers/WaypointHandler.cpp +++ b/src/handlers/WaypointHandler.cpp @@ -840,10 +840,10 @@ geoops::RoverPose WaypointHandler::SmartRetrieveRoverPose(bool bVIOTracking) } } - // Submit a debug print for the current rover pose. The pose stores both the + // Submit a debug print for the current rover pose. geoops::UTMCoordinate stCurrentUTMPosition = geoops::ConvertGPSToUTM(stCurrentVIOPosition); LOG_DEBUG(logging::g_qSharedLogger, - "Camera VIO Pose is currently: {} (northing), {} (easting), {} (alt), {} (degrees), GNSS/VIO FUSED? = {}", + "Camera VIO Pose is currently: {} (easting), {} (northing), {} (alt), {} (degrees), GNSS/VIO FUSED? = {}", stCurrentUTMPosition.dEasting, stCurrentUTMPosition.dNorthing, stCurrentUTMPosition.dAltitude, diff --git a/src/interfaces/BasicCamera.hpp b/src/interfaces/BasicCamera.hpp index d92ecf1e..d8bdd038 100644 --- a/src/interfaces/BasicCamera.hpp +++ b/src/interfaces/BasicCamera.hpp @@ -142,7 +142,7 @@ class BasicCamera : public Camera * @author clayjay3 (claytonraycowen@gmail.com) * @date 2024-12-22 ******************************************************************************/ - virtual std::future RequestFrameCopy(cv::Mat& cvFrame) override = 0; + std::future RequestFrameCopy(cv::Mat& cvFrame) override = 0; /****************************************************************************** * @brief Accessor for the cameras path or video index. diff --git a/src/interfaces/ZEDCamera.hpp b/src/interfaces/ZEDCamera.hpp index 371d05b9..dba640ac 100644 --- a/src/interfaces/ZEDCamera.hpp +++ b/src/interfaces/ZEDCamera.hpp @@ -216,7 +216,7 @@ class ZEDCamera : public Camera * @author clayjay3 (claytonraycowen@gmail.com) * @date 2024-12-22 ******************************************************************************/ - virtual std::future RequestFrameCopy(cv::Mat& cvFrame) override = 0; + std::future RequestFrameCopy(cv::Mat& cvFrame) override = 0; /****************************************************************************** * @brief Puts a frame pointer into a queue so a copy of a frame from the camera can be written to it. diff --git a/src/states/SearchPatternState.cpp b/src/states/SearchPatternState.cpp index 5b879d52..baae55ce 100644 --- a/src/states/SearchPatternState.cpp +++ b/src/states/SearchPatternState.cpp @@ -48,7 +48,7 @@ namespace statemachine m_stSearchPatternCenter = globals::g_pWaypointHandler->PeekNextWaypoint().GetGPSCoordinate(); m_vSearchPath = searchpattern::CalculateSpiralPatternWaypoints(m_stSearchPatternCenter, constants::SEARCH_ANGULAR_STEP_DEGREES, - constants::SEARCH_MAX_RADIUS * 2, + constants::SEARCH_MAX_RADIUS, stCurrentRoverPose.GetCompassHeading(), constants::SEARCH_SPIRAL_SPACING); diff --git a/src/vision/aruco/TagDetector.cpp b/src/vision/aruco/TagDetector.cpp index 5c8e80e0..a1a08af5 100644 --- a/src/vision/aruco/TagDetector.cpp +++ b/src/vision/aruco/TagDetector.cpp @@ -263,7 +263,7 @@ void TagDetector::ThreadedContinuousCode() } else if (!m_cvFrame.empty() && m_cvFrame.channels() > 3) { - // Drop the Alpha channel from the image copy to preproc frame. + // Drop the Alpha channel from the image. This is necessary for the Aruco detection. cv::cvtColor(m_cvFrame, m_cvFrame, cv::COLOR_BGRA2RGB); } } diff --git a/src/vision/cameras/sim/SIMZEDCam.cpp b/src/vision/cameras/sim/SIMZEDCam.cpp index 45380402..1698b9d2 100644 --- a/src/vision/cameras/sim/SIMZEDCam.cpp +++ b/src/vision/cameras/sim/SIMZEDCam.cpp @@ -70,8 +70,8 @@ SIMZEDCam::SIMZEDCam(const std::string szCameraPath, m_cvPointCloud = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_32FC4); // Construct camera stream objects. Append proper camera path arguments to each URL camera path. - m_pRGBStream = std::make_unique(szCameraPath, "ZEDFrontRGB"); - // m_pDepthImageStream = std::make_unique(szCameraPath, "ZEDFrontDepthImage"); + m_pRGBStream = std::make_unique(szCameraPath, "ZEDFrontRGB"); + m_pDepthImageStream = std::make_unique(szCameraPath, "ZEDFrontDepthImage"); // m_pDepthMeasureStream = std::make_unique(szCameraPath, "ZEDFrontDepthMeasure"); // Set callbacks for the WebRTC connections. @@ -118,14 +118,14 @@ void SIMZEDCam::SetCallbacks() // Deep copy the frame. m_cvFrame = cvFrame.clone(); }); - // m_pDepthImageStream->SetOnFrameReceivedCallback( - // [this](cv::Mat& cvFrame) - // { - // // Acquire a lock on the webRTC copy mutex. - // std::unique_lock lkWebRTC(m_muWebRTCDepthImageCopyMutex); - // // Deep copy the frame. - // m_cvDepthImage = cvFrame.clone(); - // }); + m_pDepthImageStream->SetOnFrameReceivedCallback( + [this](cv::Mat& cvFrame) + { + // Acquire a lock on the webRTC copy mutex. + std::unique_lock lkWebRTC(m_muWebRTCDepthImageCopyMutex); + // Deep copy the frame. + m_cvDepthImage = cvFrame.clone(); + }); // m_pDepthMeasureStream->SetOnFrameReceivedCallback( // [this](cv::Mat& cvFrame) // { @@ -137,45 +137,32 @@ void SIMZEDCam::SetCallbacks() } /****************************************************************************** - * @brief The code inside this private method runs in a separate thread, but still - * has access to this*. This method continuously get new frames from the OpenCV - * VideoCapture object and stores it in a member variable. Then a thread pool is - * started and joined once per iteration to mass copy the frames and/or measure - * to any other thread waiting in the queues. + * @brief This method decodes the encoded depth measure data from the simulator. + * We receive the depth measure from an H264 stream so it has to be encoded + * and packed in a special way to ensure things like compression and + * transmission are efficient. * + * @param cvDepthBuffer - The encoded depth buffer. + * @param cvDepthMeasure - The decoded depth measure that will be written to. * + * @note http://reality.cs.ucl.ac.uk/projects/depth-streaming/depth-streaming.pdf * * @author clayjay3 (claytonraycowen@gmail.com) - * @date 2023-09-30 + * @date 2025-01-04 ******************************************************************************/ -void SIMZEDCam::ThreadedContinuousCode() +void SIMZEDCam::DecodeDepthMeasure(const cv::Mat& cvDepthBuffer, cv::Mat& cvDepthMeasure) { - // Acquire a lock on the rover pose mutex. - std::unique_lock lkRoverPoseLock(m_muCurrentRoverPoseMutex); - // Check if the NavBoard pointer is valid. - if (globals::g_pNavigationBoard) - { - // Get the current rover pose from the NavBoard. - m_stCurrentRoverPose = geoops::RoverPose(globals::g_pNavigationBoard->GetGPSData(), globals::g_pNavigationBoard->GetHeading()); - } - // Release lock. - lkRoverPoseLock.unlock(); - - // Acquire a lock on the WebRTC mutex. - std::shared_lock lkWebRTC3(m_muWebRTCDepthMeasureCopyMutex); - // The Simulator uses this a special method of packing the depth measure data as defined in this paper. - // http://reality.cs.ucl.ac.uk/projects/depth-streaming/depth-streaming.pdf - // Here we will decode it. + // Declare instance variables. float fW = 65536.0; float fNP = 512.0; // Iterate over each pixel in the cvDepthMeasure image - for (int nY = 0; nY < m_cvDepthBuffer.rows; ++nY) + for (int nY = 0; nY < cvDepthBuffer.rows; ++nY) { - for (int nX = 0; nX < m_cvDepthBuffer.cols; ++nX) + for (int nX = 0; nX < cvDepthBuffer.cols; ++nX) { // Extract the encoded depth values - cv::Vec3b cvEncodedDepth = m_cvDepthBuffer.at(nY, nX); + cv::Vec3b cvEncodedDepth = cvDepthBuffer.at(nY, nX); // Extract encoded values float fL = cvEncodedDepth[2] / 255.0; @@ -209,31 +196,38 @@ void SIMZEDCam::ThreadedContinuousCode() fDepth = 65535.0; // Check if nY and nX are within the bounds of the depth image - if (nY < m_cvDepthMeasure.rows && nX < m_cvDepthMeasure.cols) + if (nY < cvDepthMeasure.rows && nX < cvDepthMeasure.cols) { // Store the decoded depth in the new cv::Mat. Convert cm to m. - m_cvDepthMeasure.at(nY, nX) = static_cast(fDepth / 100.0); + cvDepthMeasure.at(nY, nX) = static_cast(fDepth / 100.0); } } } - // Release lock. - lkWebRTC3.unlock(); +} - // Use the decoded depth measure to create a point cloud. No need to lock since WebRTC only uses the depth buffer, and we will only read the measure. - for (int nY = 0; nY < m_cvDepthMeasure.rows; ++nY) +/****************************************************************************** + * @brief This method calculates a point cloud from the decoded depth measure + * use some simple trig and the camera FOV. + * + * @param cvDepthMeasure - The decoded depth measure. + * @param cvPointCloud - The point cloud that will be written to. + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-04 + ******************************************************************************/ +void SIMZEDCam::CalculatePointCloud(const cv::Mat& cvDepthMeasure, cv::Mat& cvPointCloud) +{ + // Use the decoded depth measure to create a point cloud. + for (int nY = 0; nY < cvDepthMeasure.rows; ++nY) { - for (int nX = 0; nX < m_cvDepthMeasure.cols; ++nX) + for (int nX = 0; nX < cvDepthMeasure.cols; ++nX) { // Get the depth value. - float fDepth = m_cvDepthMeasure.at(nY, nX); - - // Get the horizontal and vertical FOV. - double dHorizontalFOV = m_dPropHorizontalFOV; - double dVerticalFOV = m_dPropVerticalFOV; + float fDepth = cvDepthMeasure.at(nY, nX); // Get the horizontal and vertical angles. - double dHorizontalAngle = (nX - m_cvDepthMeasure.cols / 2.0) * dHorizontalFOV / m_cvDepthMeasure.cols; - double dVerticalAngle = (nY - m_cvDepthMeasure.rows / 2.0) * dVerticalFOV / m_cvDepthMeasure.rows; + double dHorizontalAngle = (nX - cvDepthMeasure.cols / 2.0) * m_dPropHorizontalFOV / cvDepthMeasure.cols; + double dVerticalAngle = (nY - cvDepthMeasure.rows / 2.0) * m_dPropVerticalFOV / cvDepthMeasure.rows; // Convert angles to radians. double dHorizontalAngleRad = dHorizontalAngle * M_PI / 180.0; @@ -248,6 +242,46 @@ void SIMZEDCam::ThreadedContinuousCode() m_cvPointCloud.at(nY, nX) = cv::Vec4f(fX, fY, fZ, 1.0); } } +} + +/****************************************************************************** + * @brief The code inside this private method runs in a separate thread, but still + * has access to this*. This method continuously get new frames from the OpenCV + * VideoCapture object and stores it in a member variable. Then a thread pool is + * started and joined once per iteration to mass copy the frames and/or measure + * to any other thread waiting in the queues. + * + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2023-09-30 + ******************************************************************************/ +void SIMZEDCam::ThreadedContinuousCode() +{ + // Acquire a lock on the rover pose mutex. + std::unique_lock lkRoverPoseLock(m_muCurrentRoverPoseMutex); + // Check if the NavBoard pointer is valid. + if (globals::g_pNavigationBoard != nullptr) + { + // Get the current rover pose from the NavBoard. + m_stCurrentRoverPose = geoops::RoverPose(globals::g_pNavigationBoard->GetGPSData(), globals::g_pNavigationBoard->GetHeading()); + } + { + // Get the current rover pose from the NavBoard. + m_stCurrentRoverPose = geoops::RoverPose(globals::g_pNavigationBoard->GetGPSData(), globals::g_pNavigationBoard->GetHeading()); + } + // Release lock. + lkRoverPoseLock.unlock(); + + // Acquire a lock on the WebRTC mutex. + std::shared_lock lkWebRTC3(m_muWebRTCDepthMeasureCopyMutex); + // Decode the depth measure. + this->DecodeDepthMeasure(m_cvDepthBuffer, m_cvDepthMeasure); + // Release lock. + lkWebRTC3.unlock(); + + // Calculate the point cloud from the decoded depth measure. + this->CalculatePointCloud(m_cvDepthMeasure, m_cvPointCloud); // Acquire a shared_lock on the frame copy queue. std::shared_lock lkSchedulers(m_muPoolScheduleMutex); diff --git a/src/vision/cameras/sim/SIMZEDCam.h b/src/vision/cameras/sim/SIMZEDCam.h index c0ec53f3..563dc8bf 100644 --- a/src/vision/cameras/sim/SIMZEDCam.h +++ b/src/vision/cameras/sim/SIMZEDCam.h @@ -81,8 +81,9 @@ class SIMZEDCam : public ZEDCamera void ThreadedContinuousCode() override; void PooledLinearCode() override; - void SetCallbacks(); + void DecodeDepthMeasure(const cv::Mat& cvDepthBuffer, cv::Mat& cvDepthMeasure); + void CalculatePointCloud(const cv::Mat& cvDepthMeasure, cv::Mat& cvPointCloud); ///////////////////////////////////////// // Declare private member variables. diff --git a/src/vision/cameras/sim/WebRTC.cpp b/src/vision/cameras/sim/WebRTC.cpp index 96dddd96..7cc4ba8f 100644 --- a/src/vision/cameras/sim/WebRTC.cpp +++ b/src/vision/cameras/sim/WebRTC.cpp @@ -592,9 +592,6 @@ bool WebRTC::InitializeH264Decoder() ******************************************************************************/ bool WebRTC::DecodeH264BytesToCVMat(const std::vector& vH264EncodedBytes, cv::Mat& cvDecodedFrame, const AVPixelFormat eOutputPixelFormat) { - // Acquire the lock to prevent multiple threads from accessing the decoder at the same time. - std::lock_guard lkDecoder(m_muDecoderMutex); - // Initialize packet data. m_pPacket->data = const_cast(vH264EncodedBytes.data()); m_pPacket->size = static_cast(vH264EncodedBytes.size()); @@ -679,13 +676,24 @@ bool WebRTC::DecodeH264BytesToCVMat(const std::vector& vH264EncodedByte return false; } + + // Allocate buffer for the frame's data + int nRetCode = av_image_alloc(m_pFrame->data, m_pFrame->linesize, m_pFrame->width, m_pFrame->height, static_cast(m_pFrame->format), 32); + if (nRetCode < 0) + { + // Submit logger message. + LOG_WARNING(logging::g_qSharedLogger, "Failed to allocate image buffer!"); + return false; + } } + // Lock the mutex before calling sws_scale. + std::lock_guard lock(m_muDecoderMutex); // Create new mat for the decoded frame. cvDecodedFrame.create(m_pFrame->height, m_pFrame->width, CV_8UC3); - uint8_t* aDest[4] = {cvDecodedFrame.data, nullptr, nullptr, nullptr}; - int aDestLinesize[4] = {static_cast(cvDecodedFrame.step[0]), 0, 0, 0}; - sws_scale(m_pSWSContext, m_pFrame->data, m_pFrame->linesize, 0, m_pAVCodecContext->height, aDest, aDestLinesize); + std::array aDest = {cvDecodedFrame.data, nullptr, nullptr, nullptr}; + std::array aDestLinesize = {static_cast(cvDecodedFrame.step[0]), 0, 0, 0}; + sws_scale(m_pSWSContext, m_pFrame->data, m_pFrame->linesize, 0, m_pFrame->height, aDest.data(), aDestLinesize.data()); } // Calculate the time since the last key frame request. diff --git a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh index 960fd15c..48591cdd 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh @@ -21,6 +21,31 @@ else rm -rf /tmp/pkg rm -rf /tmp/ffmpeg + # Install Dependencies + apt update + apt install -y \ + libgnutls*-dev \ + libaom-dev \ + libass-dev \ + libfdk-aac-dev \ + libdav1d-dev \ + libmp3lame-dev \ + libopus-dev \ + libvorbis-dev \ + libvpx-dev \ + libx264-dev \ + libx265-dev + + # This is a workaround for the libsvtav1-dev package not being available in the repository. The package is installed manually. + git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git + cd SVT-AV1 + cd Build + cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release + make -j 8 + make install + cd ../.. + rm -rf SVT-AV1 + # Create Package Directory mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/DEBIAN @@ -41,7 +66,28 @@ else cd ffmpeg # Configure FFMPEG - ./configure --prefix=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local --enable-static --disable-shared --disable-doc --enable-gpl --enable-libx264 --enable-pic + ./configure --prefix=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local \ + --enable-static \ + --disable-shared \ + --disable-doc \ + --enable-pic \ + --extra-libs="-lpthread -lm" \ + --ld="g++" \ + --enable-gpl \ + --enable-gnutls \ + --enable-libaom \ + --enable-libass \ + --enable-libfdk-aac \ + --enable-libfreetype \ + --enable-libmp3lame \ + --enable-libopus \ + --enable-libsvtav1 \ + --enable-libdav1d \ + --enable-libvorbis \ + --enable-libvpx \ + --enable-libx264 \ + --enable-libx265 \ + --enable-nonfree # Install FFMPEG make diff --git a/tools/package-builders/libdatachannel/libdatachannel-arm64-pkg.sh b/tools/package-builders/libdatachannel/libdatachannel-arm64-pkg.sh index a5c914ad..1367fc9f 100755 --- a/tools/package-builders/libdatachannel/libdatachannel-arm64-pkg.sh +++ b/tools/package-builders/libdatachannel/libdatachannel-arm64-pkg.sh @@ -4,7 +4,7 @@ cd /tmp # Install Variables -LIBDATACHANNEL_VERSION="0.22" +LIBDATACHANNEL_VERSION="0.22.3" # Define Package URL FILE_URL="https://github.com/MissouriMRDT/Autonomy_Packages/raw/main/libdatachannel/arm64/libdatachannel_${LIBDATACHANNEL_VERSION}_arm64.deb" From 36abe97a310e1f2d432ac18b49a50b7a569b94cd Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sat, 4 Jan 2025 20:59:54 -0600 Subject: [PATCH 03/14] Some finishing touches and speed improvements. --- .devcontainer/devcontainer.json | 3 +- CMakeLists.txt | 4 ++ src/AutonomyConstants.h | 1 + src/vision/cameras/BasicCam.cpp | 8 +++ src/vision/cameras/BasicCam.h | 1 - src/vision/cameras/sim/SIMZEDCam.cpp | 58 ++++++++++++------- src/vision/cameras/sim/WebRTC.cpp | 34 ++++++++--- src/vision/cameras/sim/WebRTC.h | 3 +- .../ffmpeg/ffmpeg-amd64-pkg.sh | 22 ++++--- .../ffmpeg/ffmpeg-arm64-pkg.sh | 46 ++++++++++++++- 10 files changed, 136 insertions(+), 44 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3b46957e..9197de30 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -71,7 +71,8 @@ "gruntfuggly.todo-tree", "streetsidesoftware.code-spell-checker", "vscode-icons-team.vscode-icons", - "ryanluker.vscode-coverage-gutters" + "ryanluker.vscode-coverage-gutters", + "GitHub.vscode-pull-request-github" ], "settings": { // VSCode settings. diff --git a/CMakeLists.txt b/CMakeLists.txt index b0d16b98..4ffeaa9f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,9 @@ endif() ## Find Threads find_package(Threads REQUIRED) +## Find OpenMP +find_package(OpenMP REQUIRED) + ## Find Quill find_package(quill REQUIRED) @@ -309,6 +312,7 @@ endif() ## Link Libraries set(AUTONOMY_LIBRARIES Threads::Threads + OpenMP::OpenMP_CXX Eigen3::Eigen RoveComm_CPP quill::quill diff --git a/src/AutonomyConstants.h b/src/AutonomyConstants.h index 3b501e6d..e473952d 100755 --- a/src/AutonomyConstants.h +++ b/src/AutonomyConstants.h @@ -41,6 +41,7 @@ namespace constants const bool MODE_SIM = false; // REG MODE ENABLED: Toggle RoveComm and Cameras to use standard configuration. #endif const std::string SIM_IP_ADDRESS = "192.168.69.48"; // The IP address to use for simulation mode. + const uint SIM_WEBRTC_QP = 20; // The QP value to use for WebRTC in simulation mode. 0-51, 0 is lossless. If too high for network, frames drop. // Safety constants. const double BATTERY_MINIMUM_CELL_VOLTAGE = 3.2; // The minimum cell voltage of the battery before autonomy will forcefully enter Idle state. diff --git a/src/vision/cameras/BasicCam.cpp b/src/vision/cameras/BasicCam.cpp index 705af0e6..cc7a6c13 100644 --- a/src/vision/cameras/BasicCam.cpp +++ b/src/vision/cameras/BasicCam.cpp @@ -47,9 +47,17 @@ BasicCam::BasicCam(const std::string szCameraPath, bEnableRecordingFlag, nNumFrameRetrievalThreads) { + // Initialize the OpenCV mat to a black/empty image the size of the camera resolution. + m_cvFrame = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_8UC4); + // Set flag specifying that the camera is located at a dev/video index. m_bCameraIsConnectedOnVideoIndex = false; + // Set video cap properties. + m_cvCamera.set(cv::CAP_PROP_FRAME_WIDTH, nPropResolutionX); + m_cvCamera.set(cv::CAP_PROP_FRAME_HEIGHT, nPropResolutionY); + m_cvCamera.set(cv::CAP_PROP_FPS, nPropFramesPerSecond); + // Attempt to open camera with OpenCV's VideoCapture and print if successfully opened or not. if (m_cvCamera.open(szCameraPath)) { diff --git a/src/vision/cameras/BasicCam.h b/src/vision/cameras/BasicCam.h index 36f8c612..7fa82bd7 100644 --- a/src/vision/cameras/BasicCam.h +++ b/src/vision/cameras/BasicCam.h @@ -72,7 +72,6 @@ class BasicCam : public BasicCamera cv::VideoCapture m_cvCamera; std::string m_szCameraPath; bool m_bCameraIsConnectedOnVideoIndex; - int m_nCameraIndex; int m_nNumFrameRetrievalThreads; // Mats for storing frames. diff --git a/src/vision/cameras/sim/SIMZEDCam.cpp b/src/vision/cameras/sim/SIMZEDCam.cpp index 1698b9d2..daf179ba 100644 --- a/src/vision/cameras/sim/SIMZEDCam.cpp +++ b/src/vision/cameras/sim/SIMZEDCam.cpp @@ -17,6 +17,7 @@ /// \cond #include "../../../util/NumberOperations.hpp" #include +#include /// \endcond @@ -70,9 +71,9 @@ SIMZEDCam::SIMZEDCam(const std::string szCameraPath, m_cvPointCloud = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_32FC4); // Construct camera stream objects. Append proper camera path arguments to each URL camera path. - m_pRGBStream = std::make_unique(szCameraPath, "ZEDFrontRGB"); - m_pDepthImageStream = std::make_unique(szCameraPath, "ZEDFrontDepthImage"); - // m_pDepthMeasureStream = std::make_unique(szCameraPath, "ZEDFrontDepthMeasure"); + m_pRGBStream = std::make_unique(szCameraPath, "ZEDFrontRGB"); + m_pDepthImageStream = std::make_unique(szCameraPath, "ZEDFrontDepthImage"); + m_pDepthMeasureStream = std::make_unique(szCameraPath, "ZEDFrontDepthMeasure"); // Set callbacks for the WebRTC connections. this->SetCallbacks(); @@ -126,14 +127,14 @@ void SIMZEDCam::SetCallbacks() // Deep copy the frame. m_cvDepthImage = cvFrame.clone(); }); - // m_pDepthMeasureStream->SetOnFrameReceivedCallback( - // [this](cv::Mat& cvFrame) - // { - // // Acquire a lock on the webRTC copy mutex. - // std::unique_lock lkWebRTC(m_muWebRTCDepthMeasureCopyMutex); - // // Deep copy the frame. - // m_cvDepthBuffer = cvFrame.clone(); - // }); + m_pDepthMeasureStream->SetOnFrameReceivedCallback( + [this](cv::Mat& cvFrame) + { + // Acquire a lock on the webRTC copy mutex. + std::unique_lock lkWebRTC(m_muWebRTCDepthMeasureCopyMutex); + // Deep copy the frame. + m_cvDepthBuffer = cvFrame.clone(); + }); } /****************************************************************************** @@ -156,6 +157,9 @@ void SIMZEDCam::DecodeDepthMeasure(const cv::Mat& cvDepthBuffer, cv::Mat& cvDept float fW = 65536.0; float fNP = 512.0; +// This is a parallel for loop that decodes the depth measure from the encoded depth buffer. +#pragma omp parallel for collapse(2) + // Iterate over each pixel in the cvDepthMeasure image for (int nY = 0; nY < cvDepthBuffer.rows; ++nY) { @@ -217,6 +221,9 @@ void SIMZEDCam::DecodeDepthMeasure(const cv::Mat& cvDepthBuffer, cv::Mat& cvDept ******************************************************************************/ void SIMZEDCam::CalculatePointCloud(const cv::Mat& cvDepthMeasure, cv::Mat& cvPointCloud) { +// This is a parallel for loop that calculates the point cloud from the decoded depth measure. +#pragma omp parallel for collapse(2) + // Use the decoded depth measure to create a point cloud. for (int nY = 0; nY < cvDepthMeasure.rows; ++nY) { @@ -239,7 +246,7 @@ void SIMZEDCam::CalculatePointCloud(const cv::Mat& cvDepthMeasure, cv::Mat& cvPo float fZ = fDepth * cos(dHorizontalAngleRad) * cos(dVerticalAngleRad); // Store the decoded depth in the new cv::Mat - m_cvPointCloud.at(nY, nX) = cv::Vec4f(fX, fY, fZ, 1.0); + cvPointCloud.at(nY, nX) = cv::Vec4f(fX, fY, fZ, 1.0); } } } @@ -273,15 +280,19 @@ void SIMZEDCam::ThreadedContinuousCode() // Release lock. lkRoverPoseLock.unlock(); - // Acquire a lock on the WebRTC mutex. - std::shared_lock lkWebRTC3(m_muWebRTCDepthMeasureCopyMutex); - // Decode the depth measure. - this->DecodeDepthMeasure(m_cvDepthBuffer, m_cvDepthMeasure); - // Release lock. - lkWebRTC3.unlock(); + // Check if the depth measure WebRTC connection is open. + if (m_pDepthMeasureStream != nullptr && m_pDepthMeasureStream->GetIsConnected()) + { + // Acquire a lock on the WebRTC mutex. + std::shared_lock lkWebRTC3(m_muWebRTCDepthMeasureCopyMutex); + // Decode the depth measure. + this->DecodeDepthMeasure(m_cvDepthBuffer, m_cvDepthMeasure); + // Release lock. + lkWebRTC3.unlock(); - // Calculate the point cloud from the decoded depth measure. - this->CalculatePointCloud(m_cvDepthMeasure, m_cvPointCloud); + // Calculate the point cloud from the decoded depth measure. + this->CalculatePointCloud(m_cvDepthMeasure, m_cvPointCloud); + } // Acquire a shared_lock on the frame copy queue. std::shared_lock lkSchedulers(m_muPoolScheduleMutex); @@ -322,6 +333,11 @@ void SIMZEDCam::ThreadedContinuousCode() // Wait for thread pool to finish. this->JoinPool(); + + // Release lock on WebRTC mutex. + lkWebRTC.unlock(); + lkWebRTC2.unlock(); + lkWebRTC3.unlock(); } // Release lock on frame copy queue. @@ -729,7 +745,7 @@ void SIMZEDCam::SetPositionalPose(const double dX, const double dY, const double ******************************************************************************/ bool SIMZEDCam::GetCameraIsOpen() { - return m_pRGBStream->GetIsConnected(); //&& m_pDepthImageStream->GetIsConnected() && m_pDepthMeasureStream->GetIsConnected(); + return m_pRGBStream->GetIsConnected() && m_pDepthImageStream->GetIsConnected() && m_pDepthMeasureStream->GetIsConnected(); } /****************************************************************************** diff --git a/src/vision/cameras/sim/WebRTC.cpp b/src/vision/cameras/sim/WebRTC.cpp index 7cc4ba8f..8e14396c 100644 --- a/src/vision/cameras/sim/WebRTC.cpp +++ b/src/vision/cameras/sim/WebRTC.cpp @@ -11,8 +11,11 @@ #include "WebRTC.h" #include "../../../AutonomyLogging.h" +/// \cond #include +/// \endcond + /****************************************************************************** * @brief Construct a new Web RTC::WebRTC object. * @@ -95,6 +98,8 @@ WebRTC::~WebRTC() // Set dangling pointers to nullptr. m_pAVCodecContext = nullptr; + m_pFrame = nullptr; + m_pPacket = nullptr; m_pSWSContext = nullptr; } @@ -126,7 +131,15 @@ void WebRTC::SetOnFrameReceivedCallback(std::function fnOnFrameR ******************************************************************************/ bool WebRTC::GetIsConnected() const { - return m_pWebSocket->isOpen(); + // Check if the datachannel shared pointer is valid. + if (m_pWebSocket != nullptr) + { + // Check if the datachannel is open. + return m_pWebSocket->isOpen(); + } + + // Return false if the datachannel is not valid. + return false; } /****************************************************************************** @@ -218,7 +231,7 @@ bool WebRTC::ConnectToSignallingServer(const std::string& szSignallingServerURL) LOG_DEBUG(logging::g_qSharedLogger, "Received config message from signalling server: {}", jsnMessage.dump()); } // If the message from the server is an offer, set the remote description offer. - if (szType == "offer") + else if (szType == "offer") { // Get the SDP offer and set it as the remote description. std::string sdp = jsnMessage["sdp"]; @@ -371,6 +384,8 @@ bool WebRTC::ConnectToSignallingServer(const std::string& szSignallingServerURL) vH264EncodedBytes.push_back(static_cast(stdByte)); } + // Acquire a mutex lock on the shared_mutex before calling sws_scale. + std::unique_lock lkDecoderLock(m_muDecoderMutex); // Pass to FFmpeg decoder this->DecodeH264BytesToCVMat(vH264EncodedBytes, m_cvFrame, m_eOutputPixelFormat); @@ -380,6 +395,8 @@ bool WebRTC::ConnectToSignallingServer(const std::string& szSignallingServerURL) // Call the user's callback function. m_fnOnFrameReceivedCallback(m_cvFrame); } + // Release the lock on the shared_mutex. + lkDecoderLock.unlock(); } }); }); @@ -575,6 +592,9 @@ bool WebRTC::InitializeH264Decoder() return false; } + // Set the SWSScale context to nullptr. + m_pSWSContext = nullptr; + return true; } @@ -655,7 +675,7 @@ bool WebRTC::DecodeH264BytesToCVMat(const std::vector& vH264EncodedByte else { // Convert the decoded frame to cv::Mat using sws_scale. - if (!m_pSWSContext) + if (m_pSWSContext == nullptr) { m_pSWSContext = sws_getContext(m_pFrame->width, m_pFrame->height, @@ -667,7 +687,7 @@ bool WebRTC::DecodeH264BytesToCVMat(const std::vector& vH264EncodedByte nullptr, nullptr, nullptr); - if (!m_pSWSContext) + if (m_pSWSContext == nullptr) { // Submit logger message. LOG_WARNING(logging::g_qSharedLogger, "Failed to initialize SwsContext!"); @@ -687,12 +707,12 @@ bool WebRTC::DecodeH264BytesToCVMat(const std::vector& vH264EncodedByte } } - // Lock the mutex before calling sws_scale. - std::lock_guard lock(m_muDecoderMutex); // Create new mat for the decoded frame. cvDecodedFrame.create(m_pFrame->height, m_pFrame->width, CV_8UC3); std::array aDest = {cvDecodedFrame.data, nullptr, nullptr, nullptr}; std::array aDestLinesize = {static_cast(cvDecodedFrame.step[0]), 0, 0, 0}; + + // Convert the frame to the output pixel format. sws_scale(m_pSWSContext, m_pFrame->data, m_pFrame->linesize, 0, m_pFrame->height, aDest.data(), aDestLinesize.data()); } @@ -705,7 +725,7 @@ bool WebRTC::DecodeH264BytesToCVMat(const std::vector& vH264EncodedByte // this->RequestKeyFrame(); // Set the QP factor to 0. (Max quality) - this->SendCommandToStreamer(R"({"Encoder.MaxQP":15})"); + this->SendCommandToStreamer("{\"Encoder.MaxQP\":" + std::to_string(constants::SIM_WEBRTC_QP) + "}"); // Set the bitrate limits. this->SendCommandToStreamer(R"({"WebRTC.MinBitrate":99999})"); this->SendCommandToStreamer(R"({"WebRTC.MaxBitrate":99999999})"); diff --git a/src/vision/cameras/sim/WebRTC.h b/src/vision/cameras/sim/WebRTC.h index 8006f4e4..a2686fa1 100644 --- a/src/vision/cameras/sim/WebRTC.h +++ b/src/vision/cameras/sim/WebRTC.h @@ -15,6 +15,7 @@ #include #include #include +#include extern "C" { @@ -85,7 +86,7 @@ class WebRTC AVPacket* m_pPacket; SwsContext* m_pSWSContext; AVPixelFormat m_eOutputPixelFormat; - std::mutex m_muDecoderMutex; + std::shared_mutex m_muDecoderMutex; // OpenCV Mat for storing the frame. cv::Mat m_cvFrame; diff --git a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh index 48591cdd..9ac45abc 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh @@ -24,17 +24,16 @@ else # Install Dependencies apt update apt install -y \ - libgnutls*-dev \ - libaom-dev \ - libass-dev \ - libfdk-aac-dev \ - libdav1d-dev \ - libmp3lame-dev \ - libopus-dev \ - libvorbis-dev \ - libvpx-dev \ - libx264-dev \ - libx265-dev + libaom-dev \ + libass-dev \ + libfdk-aac-dev \ + libdav1d-dev \ + libmp3lame-dev \ + libopus-dev \ + libvorbis-dev \ + libvpx-dev \ + libx264-dev \ + libx265-dev # This is a workaround for the libsvtav1-dev package not being available in the repository. The package is installed manually. git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git @@ -74,7 +73,6 @@ else --extra-libs="-lpthread -lm" \ --ld="g++" \ --enable-gpl \ - --enable-gnutls \ --enable-libaom \ --enable-libass \ --enable-libfdk-aac \ diff --git a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh index 1dd7947b..09fcc1a0 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh @@ -21,6 +21,30 @@ else rm -rf /tmp/pkg rm -rf /tmp/ffmpeg + # Install Dependencies + apt update + apt install -y \ + libaom-dev \ + libass-dev \ + libfdk-aac-dev \ + libdav1d-dev \ + libmp3lame-dev \ + libopus-dev \ + libvorbis-dev \ + libvpx-dev \ + libx264-dev \ + libx265-dev + + # This is a workaround for the libsvtav1-dev package not being available in the repository. The package is installed manually. + git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git + cd SVT-AV1 + cd Build + cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release + make -j 8 + make install + cd ../.. + rm -rf SVT-AV1 + # Create Package Directory mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/usr/local mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/DEBIAN @@ -41,7 +65,27 @@ else cd ffmpeg # Configure FFMPEG - ./configure --prefix=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local --enable-static --disable-shared --disable-doc --enable-gpl --enable-libx264 --enable-pic + ./configure --prefix=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local \ + --enable-static \ + --disable-shared \ + --disable-doc \ + --enable-pic \ + --extra-libs="-lpthread -lm" \ + --ld="g++" \ + --enable-gpl \ + --enable-libaom \ + --enable-libass \ + --enable-libfdk-aac \ + --enable-libfreetype \ + --enable-libmp3lame \ + --enable-libopus \ + --enable-libsvtav1 \ + --enable-libdav1d \ + --enable-libvorbis \ + --enable-libvpx \ + --enable-libx264 \ + --enable-libx265 \ + --enable-nonfree # Install FFMPEG make From 31b4dec57f0101d26a4383aa7c4ce75446a748c5 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 11:05:24 -0600 Subject: [PATCH 04/14] Fix tag detector thread contention from tagutils namespace. --- src/states/ApproachingMarkerState.cpp | 28 ++--- src/states/NavigatingState.cpp | 3 +- src/states/SearchPatternState.cpp | 15 +-- src/states/SearchPatternState.h | 2 - src/util/vision/TagDetectionUtilty.hpp | 139 ++++++++----------------- src/vision/aruco/TagDetector.cpp | 2 +- 6 files changed, 57 insertions(+), 132 deletions(-) diff --git a/src/states/ApproachingMarkerState.cpp b/src/states/ApproachingMarkerState.cpp index 77a56b4c..f97c8d23 100644 --- a/src/states/ApproachingMarkerState.cpp +++ b/src/states/ApproachingMarkerState.cpp @@ -328,8 +328,9 @@ namespace statemachine bool ApproachingMarkerState::IdentifyTargetArucoMarker(arucotag::ArucoTag& stTarget) { // Load all detected tags in the rover's vision. - std::vector vDetectedTags; - tagdetectutils::LoadDetectedArucoTags(vDetectedTags, m_vTagDetectors, true); + std::vector vDetectedArucoTags; + std::vector vDetectedTensorflowTags; + tagdetectutils::LoadDetectedTags(vDetectedArucoTags, vDetectedTensorflowTags, m_vTagDetectors); arucotag::ArucoTag stBestTag; stBestTag.dStraightLineDistance = std::numeric_limits::max(); @@ -339,7 +340,7 @@ namespace statemachine std::string szIdentifiedTags = ""; // Select the tag that is the closest to the rover's current position. - for (const arucotag::ArucoTag& stCandidate : vDetectedTags) + for (const arucotag::ArucoTag& stCandidate : vDetectedArucoTags) { szIdentifiedTags += "\tID: " + std::to_string(stCandidate.nID) + " Hits: " + std::to_string(stCandidate.nHits) + "\n"; @@ -385,8 +386,9 @@ namespace statemachine bool ApproachingMarkerState::IdentifyTargetTensorflowMarker(tensorflowtag::TensorflowTag& stTarget) { // Load all detected tags in the rover's vision. - std::vector vDetectedTags; - tagdetectutils::LoadDetectedTensorflowTags(vDetectedTags, m_vTagDetectors); + std::vector vDetectedArucoTags; + std::vector vDetectedTensorflowTags; + tagdetectutils::LoadDetectedTags(vDetectedArucoTags, vDetectedTensorflowTags, m_vTagDetectors); tensorflowtag::TensorflowTag stBestTag; stBestTag.dStraightLineDistance = std::numeric_limits::max(); @@ -394,7 +396,7 @@ namespace statemachine // stBestTag.nID = -1; // Select the tag that is the closest to the rover's current position and above the confidence threshold. - for (const tensorflowtag::TensorflowTag& stCandidate : vDetectedTags) + for (const tensorflowtag::TensorflowTag& stCandidate : vDetectedTensorflowTags) { if (stCandidate.dStraightLineDistance < stBestTag.dStraightLineDistance && stCandidate.dConfidence >= constants::APPROACH_MARKER_TF_CONFIDENCE_THRESHOLD) { @@ -413,19 +415,5 @@ namespace statemachine } return bTagIdentified; - - // LEAD: Since TensorflowTag no longer has ID, commented out. - // // A tag was found. - // if (stBestTag.nID >= 0) - // { - // // Save it to the passed in reference. - // stTarget = stBestTag; - // return true; - // } - // // No target tag was found. - // else - // { - // return false; - // } } } // namespace statemachine diff --git a/src/states/NavigatingState.cpp b/src/states/NavigatingState.cpp index d3d9bbe8..e5f5c732 100644 --- a/src/states/NavigatingState.cpp +++ b/src/states/NavigatingState.cpp @@ -221,8 +221,7 @@ namespace statemachine // Get a list of the currently detected tags, and their stats. std::vector vDetectedArucoTags; std::vector vDetectedTensorflowTags; - tagdetectutils::LoadDetectedArucoTags(vDetectedArucoTags, m_vTagDetectors, false); - tagdetectutils::LoadDetectedTensorflowTags(vDetectedTensorflowTags, m_vTagDetectors); + tagdetectutils::LoadDetectedTags(vDetectedArucoTags, vDetectedTensorflowTags, m_vTagDetectors, false); // Check if we have detected any tags. if (vDetectedArucoTags.size() || vDetectedTensorflowTags.size()) diff --git a/src/states/SearchPatternState.cpp b/src/states/SearchPatternState.cpp index baae55ce..7608ea3a 100644 --- a/src/states/SearchPatternState.cpp +++ b/src/states/SearchPatternState.cpp @@ -36,8 +36,6 @@ namespace statemachine LOG_INFO(logging::g_qSharedLogger, "SearchPatternState: Scheduling next run of state logic."); // Initialize member variables. - m_nMaxDataPoints = 100; - m_vRoverPosition.reserve(m_nMaxDataPoints); m_eCurrentSearchPatternType = eSpiral; m_nSearchPathIdx = 0; @@ -124,16 +122,6 @@ namespace statemachine // Get the current rover pose. geoops::RoverPose stCurrentRoverPose = globals::g_pWaypointHandler->SmartRetrieveRoverPose(); - ////////////////////////// - /* --- Log Position --- */ - ////////////////////////// - - if (m_vRoverPosition.size() == m_nMaxDataPoints) - { - m_vRoverPosition.erase(m_vRoverPosition.begin()); - } - m_vRoverPosition.emplace_back(stCurrentRoverPose.GetUTMCoordinate().dEasting, stCurrentRoverPose.GetUTMCoordinate().dNorthing); - /* The overall flow of this state is as follows. 1. Is there a tag -> MarkerSeen @@ -151,8 +139,7 @@ namespace statemachine // Get a list of the currently detected tags, and their stats. std::vector vDetectedArucoTags; std::vector vDetectedTensorflowTags; - tagdetectutils::LoadDetectedArucoTags(vDetectedArucoTags, m_vTagDetectors, false); - tagdetectutils::LoadDetectedTensorflowTags(vDetectedTensorflowTags, m_vTagDetectors); + tagdetectutils::LoadDetectedTags(vDetectedArucoTags, vDetectedTensorflowTags, m_vTagDetectors, false); // Check if we have detected any tags. if (vDetectedArucoTags.size() || vDetectedTensorflowTags.size()) diff --git a/src/states/SearchPatternState.h b/src/states/SearchPatternState.h index 86790252..ae1a0b00 100644 --- a/src/states/SearchPatternState.h +++ b/src/states/SearchPatternState.h @@ -59,8 +59,6 @@ namespace statemachine std::vector m_vSearchPath; int m_nSearchPathIdx; SearchPatternType m_eCurrentSearchPatternType; - std::vector> m_vRoverPosition; - size_t m_nMaxDataPoints; statemachine::TimeIntervalBasedStuckDetector m_StuckDetector; protected: diff --git a/src/util/vision/TagDetectionUtilty.hpp b/src/util/vision/TagDetectionUtilty.hpp index f05627e0..b17c9980 100644 --- a/src/util/vision/TagDetectionUtilty.hpp +++ b/src/util/vision/TagDetectionUtilty.hpp @@ -35,29 +35,33 @@ namespace tagdetectutils { /****************************************************************************** - * @brief Aggregates all detected tags from each provided tag detector for OpenCV detection. + * @brief Aggregates all detected tags from each provided tag detector for both OpenCV and Tensorflow detection. * * @note When using bUnique, if you wish to prioritize one tag detector's detections over another put that tag detector earlier in the vTagDetectors. * - * @param vDetectedTags - Reference vector that will hold all of the aggregated detected tags. + * @param vDetectedArucoTags - Reference vector that will hold all of the aggregated detected Aruco tags. + * @param vDetectedTensorflowTags - Reference vector that will hold all of the aggregated detected Tensorflow tags. * @param vTagDetectors - Vector of pointers to tag detectors that will be used to request their detected tags. - * @param bUnique - Ensure vDetectedTags is a unique list of tags (unique by ID). + * @param bUnique - Ensure vDetectedArucoTags is a unique list of tags (unique by ID). * * @author JSpencerPittman (jspencerpittman@gmail.com) * @date 2024-03-07 ******************************************************************************/ - inline void LoadDetectedArucoTags(std::vector& vDetectedTags, const std::vector& vTagDetectors, bool bUnique = false) + inline void LoadDetectedTags(std::vector& vDetectedArucoTags, + std::vector& vDetectedTensorflowTags, + const std::vector& vTagDetectors, + bool bUnique = false) { // Number of tag detectors. size_t siNumTagDetectors = vTagDetectors.size(); // Initialize vectors to store detected tags temporarily. - // Using pointers the interference between vectors being updated across different threads should be minimal. - std::vector> vDetectedTagBuffers; - vDetectedTagBuffers.resize(siNumTagDetectors); + std::vector> vDetectedArucoTagBuffers(siNumTagDetectors); + std::vector> vDetectedTensorflowTagBuffers(siNumTagDetectors); - // Initialize vectors to stored detected tags - std::vector> vDetectedTagsFuture; + // Initialize vectors to store detected tags futures. + std::vector> vDetectedArucoTagsFuture; + std::vector> vDetectedTensorflowTagsFuture; // Request tags from each detector. for (size_t siIdx = 0; siIdx < siNumTagDetectors; ++siIdx) @@ -65,112 +69,60 @@ namespace tagdetectutils // Check if this tag detector is ready. if (vTagDetectors[siIdx]->GetIsReady()) { - // Request detected tags from detector. - vDetectedTagsFuture.emplace_back(vTagDetectors[siIdx]->RequestDetectedArucoTags(vDetectedTagBuffers[siIdx])); + // Request detected Aruco tags from detector. + vDetectedArucoTagsFuture.emplace_back(vTagDetectors[siIdx]->RequestDetectedArucoTags(vDetectedArucoTagBuffers[siIdx])); + // Request detected Tensorflow tags from detector. + vDetectedTensorflowTagsFuture.emplace_back(vTagDetectors[siIdx]->RequestDetectedTensorflowTags(vDetectedTensorflowTagBuffers[siIdx])); } } // Ensure all requests have been fulfilled. - // Then transfer tags from the buffer to vDetectedTags for the user to access. - for (size_t siIdx = 0; siIdx < vDetectedTagsFuture.size(); ++siIdx) + // Then transfer tags from the buffer to vDetectedArucoTags and vDetectedTensorflowTags for the user to access. + for (size_t siIdx = 0; siIdx < vDetectedArucoTagsFuture.size(); ++siIdx) { - vDetectedTagsFuture[siIdx].get(); - vDetectedTags.insert(vDetectedTags.end(), vDetectedTagBuffers[siIdx].begin(), vDetectedTagBuffers[siIdx].end()); + // Wait for the request to be fulfilled. + vDetectedArucoTagsFuture[siIdx].get(); + vDetectedTensorflowTagsFuture[siIdx].get(); + + // Loop through the detected Aruco tags and add them to the vDetectedArucoTags vector. + for (const arucotag::ArucoTag& tTag : vDetectedArucoTagBuffers[siIdx]) + { + vDetectedArucoTags.emplace_back(tTag); + } + + // Loop through the detected Tensorflow tags and add them to the vDetectedTensorflowTags vector. + for (const tensorflowtag::TensorflowTag& tTag : vDetectedTensorflowTagBuffers[siIdx]) + { + vDetectedTensorflowTags.emplace_back(tTag); + } } if (bUnique) { - // Remove all tags with a duplicate ID. - // This is done in ascending order. This means that if a user wishes to prioritize tags detected from - // specific tag detectors they should be first in the vector. + // Remove all Aruco tags with a duplicate ID. std::set setIds; size_t szIdx = 0; - while (szIdx < vDetectedTags.size()) + while (szIdx < vDetectedArucoTags.size()) { // Tag was detected by another tag detector. - if (setIds.count(vDetectedTags[szIdx].nID)) + if (setIds.count(vDetectedArucoTags[szIdx].nID)) { - vDetectedTags.erase(vDetectedTags.begin() + szIdx); + vDetectedArucoTags.erase(vDetectedArucoTags.begin() + szIdx); } else { + setIds.insert(vDetectedArucoTags[szIdx].nID); ++szIdx; } } } } - /****************************************************************************** - * @brief Aggregates all detected tags from each provided tag detector for Tensorflow detection. - * - * @note When using bUnique, if you wish to prioritize one tag detector's detections over another put that tag detector earlier in the vTagDetectors. - * - * @param vDetectedTags - Reference vector that will hold all of the aggregated detected tags. - * @param vTagDetectors - Vector of pointers to tag detectors that will be used to request their detected tags. - * - * @author JSpencerPittman (jspencerpittman@gmail.com) - * @date 2024-03-07 - ******************************************************************************/ - inline void LoadDetectedTensorflowTags(std::vector& vDetectedTags, const std::vector& vTagDetectors) - { - // Number of tag detectors. - size_t siNumTagDetectors = vTagDetectors.size(); - - // Initialize vectors to store detected tags temporarily. - // Using pointers the interference between vectors being updated across different threads should be minimal. - std::vector> vDetectedTagBuffers; - vDetectedTagBuffers.resize(siNumTagDetectors); - - // Initialize vectors to stored detected tags - std::vector> vDetectedTagsFuture; - - // Request tags from each detector. - for (size_t siIdx = 0; siIdx < siNumTagDetectors; ++siIdx) - { - // Check if this tag detector is ready. - if (vTagDetectors[siIdx]->GetIsReady()) - { - // Request detected tags from detector. - vDetectedTagsFuture.emplace_back(vTagDetectors[siIdx]->RequestDetectedTensorflowTags(vDetectedTagBuffers[siIdx])); - } - } - - // Ensure all requests have been fulfilled. - // Then transfer tags from the buffer to vDetectedTags for the user to access. - for (size_t siIdx = 0; siIdx < vDetectedTagsFuture.size(); ++siIdx) - { - vDetectedTagsFuture[siIdx].get(); - vDetectedTags.insert(vDetectedTags.end(), vDetectedTagBuffers[siIdx].begin(), vDetectedTagBuffers[siIdx].end()); - } - - // LEAD: Commented out since TensorflowTag no longer has ID. - // if (bUnique) - // { - // // Remove all tags with a duplicate ID. - // // This is done in ascending order. This means that if a user wishes to prioritize tags detected from - // // specific tag detectors they should be first in the vector. - // std::set setIds; - // size_t szIdx = 0; - // while (szIdx < vDetectedTags.size()) - // { - // // Tag was detected by another tag detector. - // if (setIds.count(vDetectedTags[szIdx].nID)) - // { - // vDetectedTags.erase(vDetectedTags.begin() + szIdx); - // } - // else - // { - // ++szIdx; - // } - // } - // } - } - /****************************************************************************** * @brief Find a tag in the rover's vision with the specified ID, using OpenCV detection. * * @param nID - The ID of the tag being looked for. - * @param tIdentifiedTag - Reference to save the identified tag. + * @param stIdentifiedArucoTag - Reference to save the identified tag. * @param vTagDetectors - Vector of pointers to tag detectors that will be used to request their detected tags. * @return true - The tag was found. * @return false - The tag was not found. @@ -178,19 +130,20 @@ namespace tagdetectutils * @author JSpencerPittman (jspencerpittman@gmail.com) * @date 2024-03-08 ******************************************************************************/ - inline bool FindArucoTagByID(int nID, arucotag::ArucoTag& stIdentifiedTag, const std::vector& vTagDetectors) + inline bool FindArucoTagByID(int nID, arucotag::ArucoTag& stIdentifiedArucoTag, const std::vector& vTagDetectors) { // Load all detected tags in the rover's vision. - std::vector vDetectedTags; - LoadDetectedArucoTags(vDetectedTags, vTagDetectors, true); + std::vector vDetectedArucoTags; + std::vector vDetectedTensorflowTags; + LoadDetectedTags(vDetectedArucoTags, vDetectedTensorflowTags, vTagDetectors, true); // Find the tag with the corresponding id. - for (const arucotag::ArucoTag& tTag : vDetectedTags) + for (const arucotag::ArucoTag& tTag : vDetectedArucoTags) { // Is this the tag being searched for. if (tTag.nID == nID) { - stIdentifiedTag = tTag; + stIdentifiedArucoTag = tTag; return true; } } diff --git a/src/vision/aruco/TagDetector.cpp b/src/vision/aruco/TagDetector.cpp index a1a08af5..7f517b7a 100644 --- a/src/vision/aruco/TagDetector.cpp +++ b/src/vision/aruco/TagDetector.cpp @@ -342,7 +342,7 @@ void TagDetector::ThreadedContinuousCode() // Check if the detected tag copy queue is empty. if (!m_qDetectedArucoTagCopySchedule.empty() || !m_qDetectedTensorflowTagCopySchedule.empty() || !m_qDetectedTagDrawnOverlayFrames.empty()) { - size_t siQueueLength = std::max({m_qDetectedArucoTagCopySchedule.size(), m_qDetectedTensorflowTagCopySchedule.size(), m_qDetectedTagDrawnOverlayFrames.size()}); + size_t siQueueLength = m_qDetectedArucoTagCopySchedule.size() + m_qDetectedTensorflowTagCopySchedule.size() + m_qDetectedTagDrawnOverlayFrames.size(); // Start the thread pool to store multiple copies of the detected tags to the requesting threads this->RunDetachedPool(siQueueLength, m_nNumDetectedTagsRetrievalThreads); // Wait for thread pool to finish. From 126af7406aa64990e05c3316fcf1116c26a73abb Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 11:05:46 -0600 Subject: [PATCH 05/14] Write simple unit tests for cameras. --- CMakeLists.txt | 7 +- src/AutonomyConstants.h | 8 +- src/interfaces/BasicCamera.hpp | 11 +- src/vision/cameras/BasicCam.cpp | 23 +-- src/vision/cameras/BasicCam.h | 5 +- src/vision/cameras/sim/SIMBasicCam.cpp | 7 +- src/vision/cameras/sim/SIMBasicCam.h | 6 +- src/vision/cameras/sim/SIMZEDCam.cpp | 6 +- src/vision/cameras/sim/WebRTC.cpp | 30 +++- .../{ => vision}/aruco/TagDetectionOpenCV.cc | 6 +- tests/Unit/src/vision/cameras/BasicCam.cc | 98 ++++++++++++ tests/Unit/src/vision/cameras/ZEDCam.cc | 83 ++++++++++ .../src/vision/cameras/sim/SIMBasicCam.cc | 87 +++++++++++ .../Unit/src/vision/cameras/sim/SIMZEDCam.cc | 143 ++++++++++++++++++ tests/Unit/src/vision/cameras/sim/WebRTC.cc | 69 +++++++++ 15 files changed, 540 insertions(+), 49 deletions(-) rename tests/Unit/src/{ => vision}/aruco/TagDetectionOpenCV.cc (97%) create mode 100644 tests/Unit/src/vision/cameras/BasicCam.cc create mode 100644 tests/Unit/src/vision/cameras/ZEDCam.cc create mode 100644 tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc create mode 100644 tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc create mode 100644 tests/Unit/src/vision/cameras/sim/WebRTC.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ffeaa9f..b3b7705f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -347,15 +347,16 @@ if (BUILD_TESTS_MODE) file(GLOB_RECURSE IntegrationTests_SRC CONFIGURE_DEPENDS "tests/Integration/*.cc") file(GLOB_RECURSE Algorithms_SRC CONFIGURE_DEPENDS "src/algorithms/*.cpp") file(GLOB_RECURSE Drivers_SRC CONFIGURE_DEPENDS "src/drivers/*.cpp") - # file(GLOB_RECURSE Handlers_SRC CONFIGURE_DEPENDS "src/handlers/*.cpp") + file(GLOB_RECURSE Vision_SRC CONFIGURE_DEPENDS "src/vision/*.cpp") file(GLOB Network_SRC CONFIGURE_DEPENDS "src/AutonomyNetworking.cpp") file(GLOB Logging_SRC CONFIGURE_DEPENDS "src/AutonomyLogging.cpp") + file(GLOB Globals_SRC CONFIGURE_DEPENDS "src/AutonomyGlobals.cpp") list(LENGTH UnitTests_SRC UnitTests_LEN) list(LENGTH IntegrationTests_SRC IntegrationTests_LEN) if (UnitTests_LEN GREATER 0) - add_executable(${EXE_NAME}_UnitTests ${UnitTests_SRC} ${Algorithms_SRC} ${Drivers_SRC} ${Network_SRC} ${Logging_SRC}) + add_executable(${EXE_NAME}_UnitTests ${UnitTests_SRC} ${Algorithms_SRC} ${Drivers_SRC} ${Vision_SRC} ${Network_SRC} ${Logging_SRC} ${Globals_SRC}) target_link_libraries(${EXE_NAME}_UnitTests GTest::gtest GTest::gtest_main ${AUTONOMY_LIBRARIES}) add_test(Unit_Tests ${EXE_NAME}_UnitTests) else() @@ -363,7 +364,7 @@ if (BUILD_TESTS_MODE) endif() if (IntegrationTests_LEN GREATER 0) - add_executable(${EXE_NAME}_IntegrationTests ${IntegrationTests_SRC} ${Algorithms_SRC} ${Drivers_SRC} ${Network_SRC} ${Logging_SRC}) + add_executable(${EXE_NAME}_IntegrationTests ${IntegrationTests_SRC} ${Algorithms_SRC} ${Drivers_SRC} ${Vision_SRC} ${Network_SRC} ${Logging_SRC} ${Globals_SRC}) target_link_libraries(${EXE_NAME}_IntegrationTests GTest::gtest GTest::gtest_main ${AUTONOMY_LIBRARIES}) add_test(Integration_Tests ${EXE_NAME}_IntegrationTests) else() diff --git a/src/AutonomyConstants.h b/src/AutonomyConstants.h index e473952d..f07339e7 100755 --- a/src/AutonomyConstants.h +++ b/src/AutonomyConstants.h @@ -41,7 +41,7 @@ namespace constants const bool MODE_SIM = false; // REG MODE ENABLED: Toggle RoveComm and Cameras to use standard configuration. #endif const std::string SIM_IP_ADDRESS = "192.168.69.48"; // The IP address to use for simulation mode. - const uint SIM_WEBRTC_QP = 20; // The QP value to use for WebRTC in simulation mode. 0-51, 0 is lossless. If too high for network, frames drop. + const uint SIM_WEBRTC_QP = 25; // The QP value to use for WebRTC in simulation mode. 0-51, 0 is lossless. If too high for network, frames drop. // Safety constants. const double BATTERY_MINIMUM_CELL_VOLTAGE = 3.2; // The minimum cell voltage of the battery before autonomy will forcefully enter Idle state. @@ -225,8 +225,8 @@ namespace constants // OpenCV ArUco detection config. const cv::aruco::PredefinedDictionaryType ARUCO_DICTIONARY = cv::aruco::DICT_4X4_50; // The predefined ArUco dictionary to use for detections. - const float ARUCO_TAG_SIDE_LENGTH = 0.015f; // Size of the white borders around the tag. - const int ARUCO_VALIDATION_THRESHOLD = 20; // How many times does the tag need to be detected(hit) before being validated as an actual aruco tag. + const float ARUCO_TAG_SIDE_LENGTH = 0.015f; // Size of the white borders around the tag in meters. + const int ARUCO_VALIDATION_THRESHOLD = 10; // How many times does the tag need to be detected(hit) before being validated as an actual aruco tag. const int ARUCO_UNVALIDATED_TAG_FORGET_THRESHOLD = 5; // How many times can an unvalidated tag be missing from frame before being forgotten. const int ARUCO_VALIDATED_TAG_FORGET_THRESHOLD = 10; // How many times can a validated tag be missing from frame before being forgotten. const double ARUCO_PIXEL_THRESHOLD = 175; // Pixel value threshold for pre-process threshold mask @@ -297,7 +297,7 @@ namespace constants /////////////////////////////////////////////////////////////////////////// // Approaching Marker State - const int APPROACH_MARKER_DETECT_ATTEMPTS_LIMIT = 60; // How many consecutive failed attempts at detecting a tag before giving up on marker. + const int APPROACH_MARKER_DETECT_ATTEMPTS_LIMIT = 5; // How many consecutive failed attempts at detecting a tag before giving up on marker. const double APPROACH_MARKER_MOTOR_POWER = 0.3; // The amount of power the motors use when approaching the marker. const double APPROACH_MARKER_PROXIMITY_THRESHOLD = 2.0; // How close in meters the rover must be to the target marker before completing its approach. const double APPROACH_MARKER_TF_CONFIDENCE_THRESHOLD = 0.5; // What is the minimal confidence necessary to consider a tensorflow tag as a target. diff --git a/src/interfaces/BasicCamera.hpp b/src/interfaces/BasicCamera.hpp index d8bdd038..5e4a33c7 100644 --- a/src/interfaces/BasicCamera.hpp +++ b/src/interfaces/BasicCamera.hpp @@ -59,8 +59,9 @@ class BasicCamera : public Camera nNumFrameRetrievalThreads) { // Initialize member variables. - m_nCameraIndex = -1; - m_szCameraPath = szCameraPath; + m_nCameraIndex = -1; + m_szCameraPath = szCameraPath; + m_bCameraIsConnectedOnVideoIndex = false; } /****************************************************************************** @@ -95,8 +96,9 @@ class BasicCamera : public Camera nNumFrameRetrievalThreads) { // Initialize member variables. - m_nCameraIndex = nCameraIndex; - m_szCameraPath = ""; + m_nCameraIndex = nCameraIndex; + m_szCameraPath = ""; + m_bCameraIsConnectedOnVideoIndex = true; } /****************************************************************************** @@ -158,6 +160,7 @@ class BasicCamera : public Camera // Declare protected methods and member variables. int m_nCameraIndex; std::string m_szCameraPath; + bool m_bCameraIsConnectedOnVideoIndex; private: // Declare private methods and member variables. diff --git a/src/vision/cameras/BasicCam.cpp b/src/vision/cameras/BasicCam.cpp index cc7a6c13..e95599f6 100644 --- a/src/vision/cameras/BasicCam.cpp +++ b/src/vision/cameras/BasicCam.cpp @@ -50,9 +50,6 @@ BasicCam::BasicCam(const std::string szCameraPath, // Initialize the OpenCV mat to a black/empty image the size of the camera resolution. m_cvFrame = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_8UC4); - // Set flag specifying that the camera is located at a dev/video index. - m_bCameraIsConnectedOnVideoIndex = false; - // Set video cap properties. m_cvCamera.set(cv::CAP_PROP_FRAME_WIDTH, nPropResolutionX); m_cvCamera.set(cv::CAP_PROP_FRAME_HEIGHT, nPropResolutionY); @@ -62,12 +59,12 @@ BasicCam::BasicCam(const std::string szCameraPath, if (m_cvCamera.open(szCameraPath)) { // Submit logger message. - LOG_INFO(logging::g_qSharedLogger, "Camera {} at path/URL {} has been successfully opened.", m_cvCamera.getBackendName(), m_szCameraPath); + LOG_INFO(logging::g_qSharedLogger, "Camera {} at path/URL {} has been successfully opened.", m_cvCamera.getBackendName(), szCameraPath); } else { // Submit logger message. - LOG_ERROR(logging::g_qSharedLogger, "Unable to open camera at path/URL {}", m_szCameraPath); + LOG_ERROR(logging::g_qSharedLogger, "Unable to open camera at path/URL {}", szCameraPath); } // Set max FPS of the ThreadedContinuousCode method. @@ -113,9 +110,6 @@ BasicCam::BasicCam(const int nCameraIndex, // Initialize the OpenCV mat to a black/empty image the size of the camera resolution. m_cvFrame = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_8UC4); - // Set flag specifying that the camera is located at a dev/video index. - m_bCameraIsConnectedOnVideoIndex = true; - // Set video cap properties. m_cvCamera.set(cv::CAP_PROP_FRAME_WIDTH, nPropResolutionX); m_cvCamera.set(cv::CAP_PROP_FRAME_HEIGHT, nPropResolutionY); @@ -155,8 +149,17 @@ BasicCam::~BasicCam() // Release camera capture object. m_cvCamera.release(); - // Submit logger message. - LOG_INFO(logging::g_qSharedLogger, "Basic camera at video index {} has been successfully closed.", m_nCameraIndex); + // Check if camera was connected on a video index. + if (m_bCameraIsConnectedOnVideoIndex) + { + // Submit logger message. + LOG_INFO(logging::g_qSharedLogger, "Basic camera at video index {} has been successfully closed.", m_nCameraIndex); + } + else + { + // Submit logger message. + LOG_INFO(logging::g_qSharedLogger, "Basic camera at path/URL {} has been successfully closed.", m_szCameraPath); + } } /****************************************************************************** diff --git a/src/vision/cameras/BasicCam.h b/src/vision/cameras/BasicCam.h index 7fa82bd7..06d9bf98 100644 --- a/src/vision/cameras/BasicCam.h +++ b/src/vision/cameras/BasicCam.h @@ -62,7 +62,7 @@ class BasicCam : public BasicCamera ///////////////////////////////////////// bool GetCameraIsOpen() override; - std::string GetCameraLocation() const; + std::string GetCameraLocation() const override; private: ///////////////////////////////////////// @@ -70,9 +70,6 @@ class BasicCam : public BasicCamera ///////////////////////////////////////// // Basic Camera specific. cv::VideoCapture m_cvCamera; - std::string m_szCameraPath; - bool m_bCameraIsConnectedOnVideoIndex; - int m_nNumFrameRetrievalThreads; // Mats for storing frames. cv::Mat m_cvFrame; diff --git a/src/vision/cameras/sim/SIMBasicCam.cpp b/src/vision/cameras/sim/SIMBasicCam.cpp index be9b40c5..5d56a74c 100644 --- a/src/vision/cameras/sim/SIMBasicCam.cpp +++ b/src/vision/cameras/sim/SIMBasicCam.cpp @@ -48,9 +48,6 @@ SIMBasicCam::SIMBasicCam(const std::string szCameraPath, bEnableRecordingFlag, nNumFrameRetrievalThreads) { - // Set flag specifying that the camera is located at a dev/video index. - m_bCameraIsConnectedOnVideoIndex = false; - // Initialize OpenCV mats to a black/empty image the size of the camera resolution. m_cvFrame = cv::Mat::zeros(nPropResolutionY, nPropResolutionX, CV_8UC4); @@ -58,12 +55,12 @@ SIMBasicCam::SIMBasicCam(const std::string szCameraPath, if (m_cvCamera.open(szCameraPath)) { // Submit logger message. - LOG_DEBUG(logging::g_qSharedLogger, "SIMCamera {} at path/URL {} has been successfully opened.", m_cvCamera.getBackendName(), m_szCameraPath); + LOG_DEBUG(logging::g_qSharedLogger, "SIMCamera {} at path/URL {} has been successfully opened.", m_cvCamera.getBackendName(), szCameraPath); } else { // Submit logger message. - LOG_ERROR(logging::g_qSharedLogger, "Unable to open SIMCamera at path/URL {}", m_szCameraPath); + LOG_ERROR(logging::g_qSharedLogger, "Unable to open SIMCamera at path/URL {}", szCameraPath); } // Set max FPS of the ThreadedContinuousCode method. diff --git a/src/vision/cameras/sim/SIMBasicCam.h b/src/vision/cameras/sim/SIMBasicCam.h index 9c514dea..5e33dffb 100644 --- a/src/vision/cameras/sim/SIMBasicCam.h +++ b/src/vision/cameras/sim/SIMBasicCam.h @@ -51,8 +51,8 @@ class SIMBasicCam : public BasicCamera // Getters. ///////////////////////////////////////// - std::string GetCameraLocation() const; bool GetCameraIsOpen() override; + std::string GetCameraLocation() const override; private: ///////////////////////////////////////// @@ -61,10 +61,6 @@ class SIMBasicCam : public BasicCamera // Basic Camera specific. cv::VideoCapture m_cvCamera; - std::string m_szCameraPath; - bool m_bCameraIsConnectedOnVideoIndex; - int m_nCameraIndex; - int m_nNumFrameRetrievalThreads; // Mats for storing frames. diff --git a/src/vision/cameras/sim/SIMZEDCam.cpp b/src/vision/cameras/sim/SIMZEDCam.cpp index daf179ba..517b633b 100644 --- a/src/vision/cameras/sim/SIMZEDCam.cpp +++ b/src/vision/cameras/sim/SIMZEDCam.cpp @@ -157,6 +157,7 @@ void SIMZEDCam::DecodeDepthMeasure(const cv::Mat& cvDepthBuffer, cv::Mat& cvDept float fW = 65536.0; float fNP = 512.0; +// TEST: Even though this speeds up the code, it might be too much CPU work as the codebase grows. Use a GpuMat instead. // This is a parallel for loop that decodes the depth measure from the encoded depth buffer. #pragma omp parallel for collapse(2) @@ -221,6 +222,7 @@ void SIMZEDCam::DecodeDepthMeasure(const cv::Mat& cvDepthBuffer, cv::Mat& cvDept ******************************************************************************/ void SIMZEDCam::CalculatePointCloud(const cv::Mat& cvDepthMeasure, cv::Mat& cvPointCloud) { +// TEST: Even though this speeds up the code, it might be too much CPU work as the codebase grows. Use a GpuMat instead. // This is a parallel for loop that calculates the point cloud from the decoded depth measure. #pragma omp parallel for collapse(2) @@ -273,10 +275,6 @@ void SIMZEDCam::ThreadedContinuousCode() // Get the current rover pose from the NavBoard. m_stCurrentRoverPose = geoops::RoverPose(globals::g_pNavigationBoard->GetGPSData(), globals::g_pNavigationBoard->GetHeading()); } - { - // Get the current rover pose from the NavBoard. - m_stCurrentRoverPose = geoops::RoverPose(globals::g_pNavigationBoard->GetGPSData(), globals::g_pNavigationBoard->GetHeading()); - } // Release lock. lkRoverPoseLock.unlock(); diff --git a/src/vision/cameras/sim/WebRTC.cpp b/src/vision/cameras/sim/WebRTC.cpp index 8e14396c..7f0cd7ac 100644 --- a/src/vision/cameras/sim/WebRTC.cpp +++ b/src/vision/cameras/sim/WebRTC.cpp @@ -60,14 +60,26 @@ WebRTC::WebRTC(const std::string& szSignallingServerURL, const std::string& szSt ******************************************************************************/ WebRTC::~WebRTC() { - // Close the video track, peer connection, data channel, and websocket. - m_pVideoTrack1->close(); - m_pPeerConnection->close(); - m_pDataChannel->close(); - m_pWebSocket->close(); + // Check if the smart pointers are valid before calling any methods. + if (m_pVideoTrack1) + { + m_pVideoTrack1->close(); + } + if (m_pPeerConnection) + { + m_pPeerConnection->close(); + } + if (m_pDataChannel) + { + m_pDataChannel->close(); + } + if (m_pWebSocket) + { + m_pWebSocket->close(); + } // Wait for all connections to close. - while (!m_pVideoTrack1->isClosed() || !m_pDataChannel->isClosed() || !m_pWebSocket->isClosed()) + while ((m_pVideoTrack1 && !m_pVideoTrack1->isClosed()) || (m_pDataChannel && !m_pDataChannel->isClosed()) || (m_pWebSocket && !m_pWebSocket->isClosed())) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } @@ -624,7 +636,11 @@ bool WebRTC::DecodeH264BytesToCVMat(const std::vector& vH264EncodedByte char aErrorBuffer[AV_ERROR_MAX_STRING_SIZE]; av_strerror(nReturnCode, aErrorBuffer, AV_ERROR_MAX_STRING_SIZE); // Submit logger message. - LOG_WARNING(logging::g_qSharedLogger, "Failed to send packet to decoder! Error code: {} {}", nReturnCode, aErrorBuffer); + LOG_NOTICE(logging::g_qSharedLogger, + "Failed to send packet to decoder! Error code: {} {}. This is not a serious problem and is likely just because some of the UDP RTP packets didn't " + "make it to us.", + nReturnCode, + aErrorBuffer); // Request a new keyframe from the video track. this->RequestKeyFrame(); diff --git a/tests/Unit/src/aruco/TagDetectionOpenCV.cc b/tests/Unit/src/vision/aruco/TagDetectionOpenCV.cc similarity index 97% rename from tests/Unit/src/aruco/TagDetectionOpenCV.cc rename to tests/Unit/src/vision/aruco/TagDetectionOpenCV.cc index c08def52..fd17e721 100644 --- a/tests/Unit/src/aruco/TagDetectionOpenCV.cc +++ b/tests/Unit/src/vision/aruco/TagDetectionOpenCV.cc @@ -8,7 +8,7 @@ * @copyright Copyright MRDT 2023 - All Rights Reserved ******************************************************************************/ -#include "../../../../src/vision/aruco/ArucoDetection.hpp" +#include "../../../../../src/vision/aruco/ArucoDetection.hpp" /// \cond #include @@ -79,7 +79,7 @@ TEST(TagDetectOpenCVTest, SingleCleanTagDetect) cv::aruco::ArucoDetector cvDetector(cvDictionary); // Load the image containing the sample ArUco tag - cv::Mat cvTestImageMat = LoadImageFromRelativePath("../../../../data/Tests/aruco/cleanArucoMarker0.png"); + cv::Mat cvTestImageMat = LoadImageFromRelativePath("../../../../../data/Tests/aruco/cleanArucoMarker0.png"); // Detect tags in the image std::vector vDetectedTags; @@ -122,7 +122,7 @@ TEST(TagDetectOpenCVTest, MultiCleanTagDetect) cv::aruco::ArucoDetector cvDetector(cvDictionary); // Load the image containing the sample ArUco tags - cv::Mat cvTestImageMat = LoadImageFromRelativePath("../../../../data/Tests/aruco/cleanArucoMarkersMultiple.png"); + cv::Mat cvTestImageMat = LoadImageFromRelativePath("../../../../../data/Tests/aruco/cleanArucoMarkersMultiple.png"); // Detect tags in the image std::vector vecDetectedTags; diff --git a/tests/Unit/src/vision/cameras/BasicCam.cc b/tests/Unit/src/vision/cameras/BasicCam.cc new file mode 100644 index 00000000..17718f76 --- /dev/null +++ b/tests/Unit/src/vision/cameras/BasicCam.cc @@ -0,0 +1,98 @@ +/****************************************************************************** + * @brief BasicCam unit tests. + * + * @file BasicCam.cc + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + * + * @copyright Copyright Mars Rover Design Team 2025 - All Rights Reserved + ******************************************************************************/ + +#include "../../../../../src/vision/cameras/BasicCam.h" + +/// \cond +#include +#include + +/// \endcond + +/****************************************************************************** + * @brief Test the functionality of the BasicCam constructor and destructor. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(BasicCamTest, ConstructorDestructor) +{ + // Create a BasicCam object. + BasicCam basicCam("/dev/video0", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + + // Check initial camera open status. + EXPECT_FALSE(basicCam.GetCameraIsOpen()); + + // Destructor will be called automatically when the object goes out of scope. +} + +/****************************************************************************** + * @brief Check that BasicCam doesn't leak any memory. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(BasicCamTest, DoesNotLeak) +{ + // Create a new BasicCam object. + BasicCam* pBasicCam = new BasicCam("/dev/video0", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + // Delete object. + delete pBasicCam; + // Point to null. + pBasicCam = nullptr; +} + +/****************************************************************************** + * @brief This should fail when the --check_for_leaks command line flag is specified. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(BasicCamTest, Leaks) +{ + // Create a new BasicCam object. + BasicCam* pBasicCam = new BasicCam("/dev/video0", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + EXPECT_TRUE(pBasicCam != nullptr); +} + +/****************************************************************************** + * @brief Test the GetCameraIsOpen method. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(BasicCamTest, GetCameraIsOpen) +{ + // Create a BasicCam object. + BasicCam basicCam("/dev/video0", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + + // Check initial camera open status. + EXPECT_FALSE(basicCam.GetCameraIsOpen()); +} + +/****************************************************************************** + * @brief Test the GetCameraLocation method. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(BasicCamTest, GetCameraLocation) +{ + // Create a BasicCam object. + BasicCam basicCam("/dev/video0", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + + // Check camera location. + EXPECT_EQ(basicCam.GetCameraLocation(), "/dev/video0"); +} \ No newline at end of file diff --git a/tests/Unit/src/vision/cameras/ZEDCam.cc b/tests/Unit/src/vision/cameras/ZEDCam.cc new file mode 100644 index 00000000..a6d05e7d --- /dev/null +++ b/tests/Unit/src/vision/cameras/ZEDCam.cc @@ -0,0 +1,83 @@ +/****************************************************************************** + * @brief ZEDCam unit tests. + * + * @file ZEDCam.cc + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + * + * @copyright Copyright Mars Rover Design Team 2025 - All Rights Reserved + ******************************************************************************/ + +#include "../../../../../src/vision/cameras/ZEDCam.h" + +/// \cond +#include +#include +#include + +/// \endcond + +/****************************************************************************** + * @brief Test the functionality of the ZEDCam constructor and destructor. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(ZEDCamTest, ConstructorDestructor) +{ + // Create a ZEDCam object. + ZEDCam zedCam(1280, 720, 30, 90.0, 60.0, false, 0.5f, 20.0f, false, false, false, 1, 0); + + // Check initial camera open status. + EXPECT_FALSE(zedCam.GetCameraIsOpen()); + + // Destructor will be called automatically when the object goes out of scope. +} + +/****************************************************************************** + * @brief Check that ZEDCam doesn't leak any memory. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(ZEDCamTest, DoesNotLeak) +{ + // Create a new ZEDCam object. + ZEDCam* pZEDCam = new ZEDCam(1280, 720, 30, 90.0, 60.0, false, 0.5f, 20.0f, false, false, false, 1, 0); + // Delete object. + delete pZEDCam; + // Point to null. + pZEDCam = nullptr; +} + +/****************************************************************************** + * @brief This should fail when the --check_for_leaks command line flag is specified. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(ZEDCamTest, Leaks) +{ + // Create a new ZEDCam object. + ZEDCam* pZEDCam = new ZEDCam(1280, 720, 30, 90.0, 60.0, false, 0.5f, 20.0f, false, false, false, 1, 0); + EXPECT_TRUE(pZEDCam != nullptr); +} + +/****************************************************************************** + * @brief Test the GetCameraIsOpen method. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(ZEDCamTest, GetCameraIsOpen) +{ + // Create a ZEDCam object. + ZEDCam zedCam(1280, 720, 30, 90.0, 60.0, false, 0.5f, 20.0f, false, false, false, 1, 0); + + // Check initial camera open status. + EXPECT_FALSE(zedCam.GetCameraIsOpen()); +} diff --git a/tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc b/tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc new file mode 100644 index 00000000..ec50023d --- /dev/null +++ b/tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc @@ -0,0 +1,87 @@ +/****************************************************************************** + * @brief SIMBasicCam unit tests. + * + * @file test_SIMBasicCam.cc + * @date 2025-01-05 + * + * @copyright Copyright Mars Rover Design Team 2025 - All Rights Reserved + ******************************************************************************/ + +#include "../../../../../../src/vision/cameras/sim/SIMBasicCam.h" + +/// \cond +#include +#include + +/// \endcond + +/****************************************************************************** + * @brief Test the functionality of the SIMBasicCam constructor and destructor. + * + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMBasicCamTest, ConstructorDestructor) +{ + // Create a SIMBasicCam object. + SIMBasicCam simBasicCam("ws://127.0.0.1:80", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + + // Check initial camera open status. + EXPECT_FALSE(simBasicCam.GetCameraIsOpen()); + + // Destructor will be called automatically when the object goes out of scope. +} + +/****************************************************************************** + * @brief Check that SIMBasicCam doesn't leak any memory. + * + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMBasicCamTest, DoesNotLeak) +{ + // Create a new SIMBasicCam object. + SIMBasicCam* pSIMBasicCam = new SIMBasicCam("ws://127.0.0.1:80", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + // Delete object. + delete pSIMBasicCam; + // Point to null. + pSIMBasicCam = nullptr; +} + +/****************************************************************************** + * @brief This should fail when the --check_for_leaks command line flag is specified. + * + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMBasicCamTest, Leaks) +{ + // Create a new SIMBasicCam object. + SIMBasicCam* pSIMBasicCam = new SIMBasicCam("ws://127.0.0.1:80", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + EXPECT_TRUE(pSIMBasicCam != nullptr); +} + +/****************************************************************************** + * @brief Test the GetCameraIsOpen method. + * + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMBasicCamTest, GetCameraIsOpen) +{ + // Create a SIMBasicCam object. + SIMBasicCam simBasicCam("ws://127.0.0.1:80", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + + // Check initial camera open status. + EXPECT_FALSE(simBasicCam.GetCameraIsOpen()); +} + +/****************************************************************************** + * @brief Test the GetCameraLocation method. + * + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMBasicCamTest, GetCameraLocation) +{ + // Create a SIMBasicCam object. + SIMBasicCam simBasicCam("ws://127.0.0.1:80", 1280, 720, 30, PIXEL_FORMATS::eBGRA, 90.0, 60.0, false, 1); + + // Check camera location. + EXPECT_EQ(simBasicCam.GetCameraLocation(), "ws://127.0.0.1:80"); +} \ No newline at end of file diff --git a/tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc b/tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc new file mode 100644 index 00000000..fddbd2bf --- /dev/null +++ b/tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc @@ -0,0 +1,143 @@ +/****************************************************************************** + * @brief SIMZEDCam unit tests. + * + * @file SIMZEDCam.cc + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + * + * @copyright Copyright Mars Rover Design Team 2025 - All Rights Reserved + ******************************************************************************/ + +#include "../../../../../../src/vision/cameras/sim/SIMZEDCam.h" + +/// \cond +#include +#include + +/// \endcond + +/****************************************************************************** + * @brief Test the functionality of the SIMZEDCam constructor and destructor. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMZEDCamTest, ConstructorDestructor) +{ + // Create a SIMZEDCam object. + SIMZEDCam simZEDCam("/dev/video0", 1280, 720, 30, 90.0, 60.0, false, 1, 12345); + + // Check initial camera open status. + EXPECT_FALSE(simZEDCam.GetCameraIsOpen()); + + // Destructor will be called automatically when the object goes out of scope. +} + +/****************************************************************************** + * @brief Check that SIMZEDCam doesn't leak any memory. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMZEDCamTest, DoesNotLeak) +{ + // Create a new SIMZEDCam object. + SIMZEDCam* pSimZEDCam = new SIMZEDCam("/dev/video0", 1280, 720, 30, 90.0, 60.0, false, 1, 12345); + // Delete object. + delete pSimZEDCam; + // Point to null. + pSimZEDCam = nullptr; +} + +/****************************************************************************** + * @brief This should fail when the --check_for_leaks command line flag is specified. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMZEDCamTest, Leaks) +{ + // Create a new SIMZEDCam object. + SIMZEDCam* pSimZEDCam = new SIMZEDCam("/dev/video0", 1280, 720, 30, 90.0, 60.0, false, 1, 12345); + EXPECT_TRUE(pSimZEDCam != nullptr); +} + +/****************************************************************************** + * @brief Test the ResetPositionalTracking method. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMZEDCamTest, ResetPositionalTracking) +{ + // Create a SIMZEDCam object. + SIMZEDCam simZEDCam("/dev/video0", 1280, 720, 30, 90.0, 60.0, false, 1, 12345); + + // Call ResetPositionalTracking. + auto errorCode = simZEDCam.ResetPositionalTracking(); + + // Check that the error code is SUCCESS. + EXPECT_EQ(errorCode, sl::ERROR_CODE::SUCCESS); +} + +/****************************************************************************** + * @brief Test the RebootCamera method. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMZEDCamTest, RebootCamera) +{ + // Create a SIMZEDCam object. + SIMZEDCam simZEDCam("/dev/video0", 1280, 720, 30, 90.0, 60.0, false, 1, 12345); + + // Call RebootCamera. + auto errorCode = simZEDCam.RebootCamera(); + + // Check that the error code is SUCCESS. + EXPECT_EQ(errorCode, sl::ERROR_CODE::SUCCESS); +} + +/****************************************************************************** + * @brief Test the EnablePositionalTracking method. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMZEDCamTest, EnablePositionalTracking) +{ + // Create a SIMZEDCam object. + SIMZEDCam simZEDCam("/dev/video0", 1280, 720, 30, 90.0, 60.0, false, 1, 12345); + + // Call EnablePositionalTracking. + auto errorCode = simZEDCam.EnablePositionalTracking(1.0f); + + // Check that the error code is SUCCESS. + EXPECT_EQ(errorCode, sl::ERROR_CODE::SUCCESS); +} + +/****************************************************************************** + * @brief Test the DisablePositionalTracking method. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(SIMZEDCamTest, DisablePositionalTracking) +{ + // Create a SIMZEDCam object. + SIMZEDCam simZEDCam("/dev/video0", 1280, 720, 30, 90.0, 60.0, false, 1, 12345); + + // Call DisablePositionalTracking. + simZEDCam.DisablePositionalTracking(); + + // Check that positional tracking is disabled. + // Assuming there is a method to check this, e.g., IsPositionalTrackingEnabled(). + EXPECT_FALSE(simZEDCam.GetPositionalTrackingEnabled()); +} \ No newline at end of file diff --git a/tests/Unit/src/vision/cameras/sim/WebRTC.cc b/tests/Unit/src/vision/cameras/sim/WebRTC.cc new file mode 100644 index 00000000..f740b59c --- /dev/null +++ b/tests/Unit/src/vision/cameras/sim/WebRTC.cc @@ -0,0 +1,69 @@ +/****************************************************************************** + * @brief WebRTC unit tests. + * + * @file WebRTC.cc + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + * + * @copyright Copyright Mars Rover Design Team 2025 - All Rights Reserved + ******************************************************************************/ + +#include "../../../../../../src/vision/cameras/sim/WebRTC.h" + +/// \cond +#include +#include +#include +#include +#include + +/// \endcond + +/****************************************************************************** + * @brief Test the functionality of the WebRTC constructor and destructor. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(WebRTCTest, ConstructorDestructor) +{ + // Create a WebRTC object. + WebRTC webrtc("ws://localhost:8080", "streamer1"); + + // Check initial connection status. + EXPECT_FALSE(webrtc.GetIsConnected()); + + // Destructor will be called automatically when the object goes out of scope. +} + +/****************************************************************************** + * @brief Check that WebRTC doesn't leak any memory. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(WebRTCTest, DoesNotLeak) +{ + // Create a new WebRTC object. + WebRTC* pWebRTC = new WebRTC("ws://localhost:8080", "streamer1"); + // Delete object. + delete pWebRTC; + // Point to null. + pWebRTC = nullptr; +} + +/****************************************************************************** + * @brief This should fail when the --check_for_leaks command line flag is specified. + * + * + * @author clayjay3 (claytonraycowen@gmail.com) + * @date 2025-01-05 + ******************************************************************************/ +TEST(WebRTCTest, Leaks) +{ + // Create a new WebRTC object. + WebRTC* pWebRTC = new WebRTC("ws://localhost:8080", "streamer1"); + EXPECT_TRUE(pWebRTC != nullptr); +} From 8759d00624a89bf5195ae0238083e17d4e9698fd Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 11:10:17 -0600 Subject: [PATCH 06/14] Update .gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e1cb219f..fb3af053 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ valgrind.rpt tools/action-runners/compose.yml !tools/package-builders/ +# Logging tools. +tools/logging/*.png + # Compiled Program Autonomy_Software From bd7c71bbaf42462d553cb681f640e9084e9cf5d3 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 11:10:34 -0600 Subject: [PATCH 07/14] Remove .png. --- tools/logging/path_with_distances.png | Bin 99858 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tools/logging/path_with_distances.png diff --git a/tools/logging/path_with_distances.png b/tools/logging/path_with_distances.png deleted file mode 100644 index b648326036548bccfa2aabe9997fbe1519cc0234..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99858 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 aJHnYyFjI)Fpy0rP4<&hZxx$-w0{#n0Icr7$ From b02e0a4b221bc77aed3312c4abe312dc55b2411a Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 11:14:46 -0600 Subject: [PATCH 08/14] Add python matplotlib to dockerfiles. This is so we can use logging scripts if we want to debug stuff while out in the field. Need to install, now cause we won't have internet out there. --- .devcontainer/Dockerfile_Jammy | 2 +- .devcontainer/Dockerfile_JetPack | 4 ++++ data/Custom_Dictionaries/Autonomy-Dictionary.txt | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile_Jammy b/.devcontainer/Dockerfile_Jammy index 94122462..c1148736 100644 --- a/.devcontainer/Dockerfile_Jammy +++ b/.devcontainer/Dockerfile_Jammy @@ -55,7 +55,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y bat \ # Install Required Python Packages and link python3 executable to python. RUN ln -s /usr/bin/python3 /usr/bin/python && \ - python -m pip install numpy opencv-python pyopengl + python -m pip install numpy opencv-python pyopengl matplotlib # Set Timezone RUN echo "${TZ}" > /etc/localtime && \ diff --git a/.devcontainer/Dockerfile_JetPack b/.devcontainer/Dockerfile_JetPack index a5a6712e..45ae5b20 100644 --- a/.devcontainer/Dockerfile_JetPack +++ b/.devcontainer/Dockerfile_JetPack @@ -45,6 +45,10 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ RUN apt-get update && apt-get install --no-install-recommends -y bat \ bash-completion fish git-lfs +# Install Required Python Packages and link python3 executable to python. +RUN ln -s /usr/bin/python3 /usr/bin/python && \ + python -m pip install numpy opencv-python pyopengl matplotlib + # This symbolic link is needed to use the streaming features on Jetson inside a container RUN ln -sf /usr/lib/aarch64-linux-gnu/tegra/libv4l2.so.0 /usr/lib/aarch64-linux-gnu/libv4l2.so diff --git a/data/Custom_Dictionaries/Autonomy-Dictionary.txt b/data/Custom_Dictionaries/Autonomy-Dictionary.txt index 1fa5f06d..fb1037b9 100644 --- a/data/Custom_Dictionaries/Autonomy-Dictionary.txt +++ b/data/Custom_Dictionaries/Autonomy-Dictionary.txt @@ -360,6 +360,7 @@ MAKEFLAGS MAPANGLE MAPRANGE marsrover +matplotlib matx Memcheck MERICA From b84ede70f6f2c3cb3c742e7454ac84f23e12dfd2 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 11:20:58 -0600 Subject: [PATCH 09/14] Maybe fix formatting. --- tests/Unit/src/vision/cameras/BasicCam.cc | 2 +- tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc | 2 +- tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Unit/src/vision/cameras/BasicCam.cc b/tests/Unit/src/vision/cameras/BasicCam.cc index 17718f76..47800afc 100644 --- a/tests/Unit/src/vision/cameras/BasicCam.cc +++ b/tests/Unit/src/vision/cameras/BasicCam.cc @@ -95,4 +95,4 @@ TEST(BasicCamTest, GetCameraLocation) // Check camera location. EXPECT_EQ(basicCam.GetCameraLocation(), "/dev/video0"); -} \ No newline at end of file +} diff --git a/tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc b/tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc index ec50023d..5237d59c 100644 --- a/tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc +++ b/tests/Unit/src/vision/cameras/sim/SIMBasicCam.cc @@ -84,4 +84,4 @@ TEST(SIMBasicCamTest, GetCameraLocation) // Check camera location. EXPECT_EQ(simBasicCam.GetCameraLocation(), "ws://127.0.0.1:80"); -} \ No newline at end of file +} diff --git a/tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc b/tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc index fddbd2bf..4f0e4f71 100644 --- a/tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc +++ b/tests/Unit/src/vision/cameras/sim/SIMZEDCam.cc @@ -140,4 +140,4 @@ TEST(SIMZEDCamTest, DisablePositionalTracking) // Check that positional tracking is disabled. // Assuming there is a method to check this, e.g., IsPositionalTrackingEnabled(). EXPECT_FALSE(simZEDCam.GetPositionalTrackingEnabled()); -} \ No newline at end of file +} From 6487320de92f3416385726e7abd3b7fce4bbbee9 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 11:24:32 -0600 Subject: [PATCH 10/14] Make deepsource happy. --- .devcontainer/Dockerfile_Jammy | 8 ++++---- .devcontainer/Dockerfile_JetPack | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.devcontainer/Dockerfile_Jammy b/.devcontainer/Dockerfile_Jammy index c1148736..356a47fb 100644 --- a/.devcontainer/Dockerfile_Jammy +++ b/.devcontainer/Dockerfile_Jammy @@ -31,16 +31,16 @@ RUN echo "${TZ}" > /etc/localtime && \ RUN echo "CUDA Version ${CUDA_MAJOR}.${CUDA_MINOR}.${CUDA_PATCH}" > /usr/local/cuda/version.txt # Add APT Repo for PCIe drivers. -RUN apt update && apt install -y curl && \ +RUN apt update && apt install -y wget gnupg && \ echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list && \ - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ - curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor >bazel-archive-keyring.gpg && \ + wget -qO - https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ + wget -qO - https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel-archive-keyring.gpg && \ mv bazel-archive-keyring.gpg /usr/share/keyrings && \ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/bazel-archive-keyring.gpg] https://storage.googleapis.com/bazel-apt stable jdk1.8" | tee /etc/apt/sources.list.d/bazel.list # Install Required Ubuntu Packages RUN apt-get update && apt-get install --no-install-recommends -y iputils-ping \ - build-essential gdb wget less udev zstd sudo libgomp1 \ + build-essential gdb less udev zstd sudo libgomp1 \ cmake git libgtk2.0-dev pkg-config libx264-dev libdrm-dev ssh \ libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev tzdata net-tools \ yasm libatlas-base-dev gfortran libpq-dev libpostproc-dev \ diff --git a/.devcontainer/Dockerfile_JetPack b/.devcontainer/Dockerfile_JetPack index 45ae5b20..f470452c 100644 --- a/.devcontainer/Dockerfile_JetPack +++ b/.devcontainer/Dockerfile_JetPack @@ -25,14 +25,14 @@ RUN echo "# R${L4T_MAJOR} (release), REVISION: ${L4T_MINOR}.${L4T_PATCH}" > /etc # Clean APT Cache RUN rm /var/lib/dpkg/info/libc-bin.* # Add APT Repo for PCIe drivers and Bazel. -RUN apt update && apt install -y curl && \ +RUN apt update && apt install -y wget && \ echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list && \ - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + wget -q -O - https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - # Install Required Ubuntu Packages RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential gfortran cmake git gdb file tar libatlas-base-dev apt-transport-https iputils-ping \ - libswresample-dev libcanberra-gtk3-module zstd wget less libx264-dev libdrm-dev \ + libswresample-dev libcanberra-gtk3-module zstd less libx264-dev libdrm-dev \ libeigen3-dev libglew-dev libgstreamer-plugins-base1.0-dev udev net-tools \ libgstreamer-plugins-good1.0-dev libgstreamer1.0-dev libgtk-3-dev libjpeg-dev sudo usbutils \ libjpeg8-dev libjpeg-turbo8-dev liblapack-dev liblapacke-dev libopenblas-dev libpng-dev tzdata \ From 4ba75936cbe66245ecf94936cad2d3807e2072b5 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 12:41:04 -0600 Subject: [PATCH 11/14] Damn you curl. --- .devcontainer/Dockerfile_Jammy | 2 +- .devcontainer/Dockerfile_JetPack | 2 +- src/AutonomyConstants.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile_Jammy b/.devcontainer/Dockerfile_Jammy index 356a47fb..18688c96 100644 --- a/.devcontainer/Dockerfile_Jammy +++ b/.devcontainer/Dockerfile_Jammy @@ -47,7 +47,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y iputils-ping \ libxine2-dev libglew-dev libtiff5-dev zlib1g-dev cowsay lolcat locales usbutils \ libeigen3-dev python3-dev python3-pip python3-numpy libx11-dev xauth libssl-dev \ valgrind doxygen graphviz htop nano fortune fortunes \ - vim-common gasket-dkms nlohmann-json3-dev gcovr lcov + vim-common gasket-dkms nlohmann-json3-dev gcovr lcov curl # Nice to have RUN apt-get update && apt-get install --no-install-recommends -y bat \ diff --git a/.devcontainer/Dockerfile_JetPack b/.devcontainer/Dockerfile_JetPack index f470452c..e8d1de60 100644 --- a/.devcontainer/Dockerfile_JetPack +++ b/.devcontainer/Dockerfile_JetPack @@ -39,7 +39,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libpostproc-dev libtbb-dev libtbb2 libtesseract-dev libtiff-dev libv4l-dev \ libxine2-dev libxvidcore-dev libx264-dev libgtkglext1 libgtkglext1-dev pkg-config qv4l2 \ v4l-utils zlib1g-dev python3-dev libboost-all-dev valgrind doxygen graphviz nano \ - vim-common libedgetpu1-std gasket-dkms ca-certificates nlohmann-json3-dev + vim-common libedgetpu1-std gasket-dkms ca-certificates nlohmann-json3-dev curl # Nice to have RUN apt-get update && apt-get install --no-install-recommends -y bat \ diff --git a/src/AutonomyConstants.h b/src/AutonomyConstants.h index f07339e7..34776328 100755 --- a/src/AutonomyConstants.h +++ b/src/AutonomyConstants.h @@ -331,7 +331,7 @@ namespace constants const double STATEMACHINE_ZED_REALIGN_THRESHOLD = 0.5; // The threshold in meters that the error between GPS and ZED must be before realigning the ZED cameras. // Navigating State. - const double NAVIGATING_REACHED_GOAL_RADIUS = 1.0; // The radius in meters that the rover should get to the goal waypoint. + const double NAVIGATING_REACHED_GOAL_RADIUS = 2.0; // The radius in meters that the rover should get to the goal waypoint. // Avoidance State. const double AVOIDANCE_STATE_MOTOR_POWER = DRIVE_MAX_POWER; // Drive speed of avoidance state From 9797533d4c9bf87c1e71a513aefbb175a8f1659e Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 14:01:41 -0600 Subject: [PATCH 12/14] Add pip to Jetpack image and fix what would be library errors from ffmpeg in both images. --- .devcontainer/Dockerfile_Jammy | 4 ++- .devcontainer/Dockerfile_JetPack | 4 ++- .devcontainer/devcontainer.json | 4 +-- .../ffmpeg/ffmpeg-amd64-pkg.sh | 34 ++++++------------ .../ffmpeg/ffmpeg-arm64-pkg.sh | 36 ++++++------------- 5 files changed, 29 insertions(+), 53 deletions(-) diff --git a/.devcontainer/Dockerfile_Jammy b/.devcontainer/Dockerfile_Jammy index 18688c96..ac02f60b 100644 --- a/.devcontainer/Dockerfile_Jammy +++ b/.devcontainer/Dockerfile_Jammy @@ -47,7 +47,9 @@ RUN apt-get update && apt-get install --no-install-recommends -y iputils-ping \ libxine2-dev libglew-dev libtiff5-dev zlib1g-dev cowsay lolcat locales usbutils \ libeigen3-dev python3-dev python3-pip python3-numpy libx11-dev xauth libssl-dev \ valgrind doxygen graphviz htop nano fortune fortunes \ - vim-common gasket-dkms nlohmann-json3-dev gcovr lcov curl + vim-common gasket-dkms nlohmann-json3-dev gcovr lcov curl \ + libaom-dev libass-dev libfdk-aac-dev libdav1d-dev libmp3lame-dev \ + libopus-dev libvorbis-dev libvpx-dev libx264-dev libx265-dev # Nice to have RUN apt-get update && apt-get install --no-install-recommends -y bat \ diff --git a/.devcontainer/Dockerfile_JetPack b/.devcontainer/Dockerfile_JetPack index e8d1de60..0bac4860 100644 --- a/.devcontainer/Dockerfile_JetPack +++ b/.devcontainer/Dockerfile_JetPack @@ -39,7 +39,9 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libpostproc-dev libtbb-dev libtbb2 libtesseract-dev libtiff-dev libv4l-dev \ libxine2-dev libxvidcore-dev libx264-dev libgtkglext1 libgtkglext1-dev pkg-config qv4l2 \ v4l-utils zlib1g-dev python3-dev libboost-all-dev valgrind doxygen graphviz nano \ - vim-common libedgetpu1-std gasket-dkms ca-certificates nlohmann-json3-dev curl + vim-common libedgetpu1-std gasket-dkms ca-certificates nlohmann-json3-dev curl \ + python3-dev python3-pip python3-numpy libaom-dev libass-dev libfdk-aac-dev libdav1d-dev \ + libmp3lame-dev libopus-dev libvorbis-dev libvpx-dev libx264-dev libx265-dev # Nice to have RUN apt-get update && apt-get install --no-install-recommends -y bat \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9197de30..7510a3b3 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -40,8 +40,8 @@ "source=${localWorkspaceFolder}/data/calibrations/zed,target=/usr/local/zed/settings,type=bind,consistency=delegated", "type=bind,readonly,source=/etc/localtime,target=/etc/localtime" ], - // "image": "ghcr.io/missourimrdt/autonomy-jammy:2024-11-27-23-04-01", - // "image": "ghcr.io/missourimrdt/autonomy-jetpack:latest", + // "image": "ghcr.io/missourimrdt/autonomy-jammy:2025-01-05-17-06-05", + // "image": "ghcr.io/missourimrdt/autonomy-jetpack:2025-01-05-17-06-05", "build": { "dockerfile": "Dockerfile_Jammy" // "dockerfile": "Dockerfile_JetPack" diff --git a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh index 9ac45abc..1fc5a71a 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh @@ -21,30 +21,6 @@ else rm -rf /tmp/pkg rm -rf /tmp/ffmpeg - # Install Dependencies - apt update - apt install -y \ - libaom-dev \ - libass-dev \ - libfdk-aac-dev \ - libdav1d-dev \ - libmp3lame-dev \ - libopus-dev \ - libvorbis-dev \ - libvpx-dev \ - libx264-dev \ - libx265-dev - - # This is a workaround for the libsvtav1-dev package not being available in the repository. The package is installed manually. - git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git - cd SVT-AV1 - cd Build - cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release - make -j 8 - make install - cd ../.. - rm -rf SVT-AV1 - # Create Package Directory mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/DEBIAN @@ -60,6 +36,16 @@ else echo "Description: A prebuilt version of ffmpeg. Made by the Mars Rover Design Team." } > /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/DEBIAN/control + # This is a workaround for the libsvtav1-dev package not being available in the repository. The package is installed manually. + git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git + cd SVT-AV1 + cd Build + cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local + make -j 8 + make install + cd ../.. + rm -rf SVT-AV1 + # Download FFMPEG git clone --recurse-submodules --depth 1 --branch n${FFMPEG_VERSION} https://github.com/FFmpeg/FFmpeg.git ffmpeg cd ffmpeg diff --git a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh index 09fcc1a0..bf3f8c2f 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh @@ -21,30 +21,6 @@ else rm -rf /tmp/pkg rm -rf /tmp/ffmpeg - # Install Dependencies - apt update - apt install -y \ - libaom-dev \ - libass-dev \ - libfdk-aac-dev \ - libdav1d-dev \ - libmp3lame-dev \ - libopus-dev \ - libvorbis-dev \ - libvpx-dev \ - libx264-dev \ - libx265-dev - - # This is a workaround for the libsvtav1-dev package not being available in the repository. The package is installed manually. - git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git - cd SVT-AV1 - cd Build - cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release - make -j 8 - make install - cd ../.. - rm -rf SVT-AV1 - # Create Package Directory mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/usr/local mkdir -p /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/DEBIAN @@ -60,12 +36,22 @@ else echo "Description: A prebuilt version of ffmpeg. Made by the Mars Rover Design Team." } > /tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/DEBIAN/control + # This is a workaround for the libsvtav1-dev package not being available in the repository. The package is installed manually. + git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git + cd SVT-AV1 + cd Build + cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/usr/local + make -j 8 + make install + cd ../.. + rm -rf SVT-AV1 + # Download FFMPEG git clone --recurse-submodules --depth 1 --branch n${FFMPEG_VERSION} https://github.com/FFmpeg/FFmpeg.git ffmpeg cd ffmpeg # Configure FFMPEG - ./configure --prefix=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local \ + ./configure --prefix=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/usr/local \ --enable-static \ --disable-shared \ --disable-doc \ From 2a83ae338a1e1b9f636dd869495c5be83f369728 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 14:21:25 -0600 Subject: [PATCH 13/14] Readd dep installs to ffmpeg package builder. --- tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh | 14 ++++++++++++++ tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh index 1fc5a71a..77441415 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh @@ -17,6 +17,20 @@ else echo "Package version ${FFMPEG_VERSION} does not exist in the repository. Building the package." echo "rebuilding_pkg=true" >> $GITHUB_OUTPUT + # Install Dependencies + apt update + apt install -y \ + libaom-dev \ + libass-dev \ + libfdk-aac-dev \ + libdav1d-dev \ + libmp3lame-dev \ + libopus-dev \ + libvorbis-dev \ + libvpx-dev \ + libx264-dev \ + libx265-dev + # Delete Old Packages rm -rf /tmp/pkg rm -rf /tmp/ffmpeg diff --git a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh index bf3f8c2f..9987b6ae 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh @@ -17,6 +17,20 @@ else echo "Package version ${FFMPEG_VERSION} does not exist in the repository. Building the package." echo "rebuilding_pkg=true" >> $GITHUB_OUTPUT + # Install Dependencies + apt update + apt install -y \ + libaom-dev \ + libass-dev \ + libfdk-aac-dev \ + libdav1d-dev \ + libmp3lame-dev \ + libopus-dev \ + libvorbis-dev \ + libvpx-dev \ + libx264-dev \ + libx265-dev + # Delete Old Packages rm -rf /tmp/pkg rm -rf /tmp/ffmpeg From 477cb221cbb47968077b1322dcb3c6120bd84da0 Mon Sep 17 00:00:00 2001 From: clayjay3 Date: Sun, 5 Jan 2025 14:37:07 -0600 Subject: [PATCH 14/14] Update FFMPEG package. --- tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh | 4 ++++ tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh index 77441415..c5064578 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-amd64-pkg.sh @@ -54,6 +54,10 @@ else git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git cd SVT-AV1 cd Build + # We need to install to system first. Then we can install to the package directory. + cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release + make -j 8 + make install cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_amd64/usr/local make -j 8 make install diff --git a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh index 9987b6ae..c0ecb2ca 100755 --- a/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh +++ b/tools/package-builders/ffmpeg/ffmpeg-arm64-pkg.sh @@ -54,6 +54,10 @@ else git clone --depth=1 https://gitlab.com/AOMediaCodec/SVT-AV1.git cd SVT-AV1 cd Build + # We need to install to system first. Then we can install to the package directory. + cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release + make -j 8 + make install cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/tmp/pkg/ffmpeg_${FFMPEG_VERSION}_arm64/usr/local make -j 8 make install