Jetson进行旋转目标检测推理实现大疆无人机飞行控制
源码结构
大疆PSDK
源码地址:
https://github.com/dji-sdk/Payload-SDK
其目录结构如下:
Payload-SDK-master
├── CMakeLists.txt
├── doc
│ ├── dji_sdk_code_style
│ └── simple_model
├── EULA.txt
├── LICENSE.txt
├── psdk_lib
│ ├── include
│ └── lib
├── README.md
├── samples
│ ├── sample_c
│ └── sample_c++
└── tools└── file2c
- CMakeLists.txt:编译文件,用 Cmake 构建工程。
- dji_sdk_code_style:SDK 代码相关文档,大致介绍了 PSDK 代码风格。
- simple_model:其中 simple_model 中包含了无人机、PSDK 开发组件、OSDK 转接板的 3D 模型图,便于完成开发后定型产品结构。
- EULA.txt:最终用户许可协议(end-user license agreements)。
- LICENSE.txt:LICENSE 说明,内部包含了一个完整 tree 结构图。
- psdk_lib: PSDK 的核心库文件,PSDK 3.X 以闭源库方式提供,在此文件夹下提供了各个平台的库(lib)以及头文件(include)。
- README.md:说明文件,包括版本更新和功能更新的信息。
- samples:PSDK 官方 sample 代码示例,此部分 sample 代码均以开源代码呈现,可以通过 sample 来学习 API 的调用方法,以及功能实现的参考逻辑。
- tools:第三方工具,提供了一个 file2c 的工具。PSDK 中 FreeRTOS 平台使用 widget 功能会需要使用此工具将文件进行转换
其中,我们要进行代码开发主要是在samples/sample_c++
文件夹
这里我们使用YOLOv8
实现选中目标检测,其具体需要执行如下过程:
Yolov8Obb
类定义
class Yolov8Obb {public:Yolov8Obb() {} ~Yolov8Obb() {} bool ReadModel(cv::dnn::Net& net, std::string& netPath, bool isCuda);bool Detect(cv::Mat& srcImg, cv::dnn::Net& net, std::vector<OutputParams>& output,int distance);int _netWidth = 640; //图片输入宽度int _netHeight = 640; //图片输入高度//类别名,自己的模型需要修改此项std::vector<std::string> _className ={"line","chuizi"};private:float _classThreshold = 0.3;float _nmsThreshold = 0.5;
};
🔹 类定义:class Yolov8Obb
- 定义一个类,名字叫
Yolov8Obb
。
🔹 构造函数和析构函数
public:Yolov8Obb() { }~Yolov8Obb() {}
Yolov8Obb()
:构造函数,创建对象时调用。目前为空,没有初始化操作。~Yolov8Obb()
:析构函数,销毁对象时调用。目前也为空。
🔹 成员函数 1:ReadModel
bool ReadModel(cv::dnn::Net& net, std::string& netPath, bool isCuda);
- 功能:加载
YOLOv8
模型文件(如.onnx
或.pb
文件)。 - 参数说明:
cv::dnn::Net& net
:OpenCV
的DNN
网络对象,用来承载加载的模型。std::string& netPath
:模型文件路径(例如"yolov8_obb.onnx"
)。bool isCuda
:是否使用GPU
(CUDA
)加速推理。
- 返回值:
true
表示加载成功,false
失败。
具体实现代码:
bool Yolov8Obb::ReadModel(cv::dnn::Net& net, std::string& netPath, bool isCuda = false) {try {if (!CheckModelPath(netPath))return false;#if CV_VERSION_MAJOR==4 &&CV_VERSION_MINOR<7std::cout << "OBB Need OpenCV Version >=4.7.0" << std::endl;return false;#endifnet = cv::dnn::readNetFromONNX(netPath);#if CV_VERSION_MAJOR==4 &&CV_VERSION_MINOR==7&&CV_VERSION_REVISION==0net.enableWinograd(false);#endif}catch (const std::exception&) {return false;}if (isCuda) {net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA); //or DNN_TARGET_CUDA_FP16}else {std::cout << "Inference device: CPU" << std::endl;net.setPreferableBackend(cv::dnn::DNN_BACKEND_DEFAULT);net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);}return true;
}
✅ 示例用法:
cv::dnn::Net net; std::string path = "yolov8_obb.onnx"; yolov8obb.ReadModel(net, path, true); // 使用 GPU
🔹 成员函数 2:Detect
bool Detect(cv::Mat& srcImg, cv::dnn::Net& net, std::vector<OutputParams>& output, int distance);
- 功能:对输入图像进行目标检测。
- 参数说明:
cv::Mat& srcImg
:输入的原始图像(OpenCV
的Mat
格式)。cv::dnn::Net& net
:已加载的YOLOv8
模型。std::vector<OutputParams>& output
:检测结果输出,包含每个检测到的物体的类别、置信度、旋转框坐标等。OutputParams
是一个自定义结构体(未在代码中给出,但通常包含:类别、置信度、中心点、宽高、角度等)。
int distance
:可能用于计算目标距离或缩放参数(具体用途需看实现)。
- 返回值:
true
表示检测成功。
旋转检测框的结构体定义如下:
struct OutputParams {int id; float confidence; cv::Rect box; cv::RotatedRect rotatedBox; //OpenCV 中的一个内置结构体类型,专门用来表示一个带有旋转角度的矩形框 cv::Mat boxMask; std::vector<PoseKeyPoint> keyPoints;
};
Detect
方法具体实现:
bool Yolov8Obb::Detect(cv::Mat& srcImg, cv::dnn::Net& net, std::vector<OutputParams>& output,int distance=0) {cv::Mat blob;output.clear();// 设置网络使用 CUDA 后端net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);int col = srcImg.cols;int row = srcImg.rows;cv::Mat netInputImg;cv::Vec4d params;LetterBox(srcImg, netInputImg, params, cv::Size(_netWidth, _netHeight));// 预处理:blobFromImagecv::dnn::blobFromImage(netInputImg, blob, 1 / 255.0, cv::Size(_netWidth, _netHeight), cv::Scalar(), true, false);net.setInput(blob);// GPU 推理输出std::vector<cv::Mat> net_output_img;net.forward(net_output_img, net.getUnconnectedOutLayersNames()); // GPU 自动执行std::vector<int> class_ids;std::vector<float> confidences;std::vector<cv::RotatedRect> boxes;// 假设输出是 [bs, 21504, 20]cv::Mat output0 = cv::Mat(cv::Size(net_output_img[0].size[2], net_output_img[0].size[1]), CV_32F, (float*)net_output_img[0].data).t();int rows = output0.rows; // 21504int net_width = output0.cols; // 20int class_score_length = net_width - 5; // 15 classesint angle_index = net_width - 1; // 最后一列是 anglefloat* pdata = (float*)output0.data;for (int r = 0; r < rows; ++r) {cv::Mat scores(1, class_score_length, CV_32FC1, pdata + 4);cv::Point classIdPoint;double max_class_socre;minMaxLoc(scores, 0, &max_class_socre, 0, &classIdPoint);max_class_socre = (float)max_class_socre;if (max_class_socre >= _classThreshold) {//rect [x,y,w,h]float x = (pdata[0] - params[2]) / params[0];float y = (pdata[1] - params[3]) / params[1];float w = pdata[2] / params[0];float h = pdata[3] / params[1];float angle = pdata[angle_index] / CV_PI *180.0;class_ids.push_back(classIdPoint.x);confidences.push_back(max_class_socre);boxes.push_back(cv::RotatedRect(cv::Point2f(x, y), cv::Size(w, h), angle));}pdata += net_width;}//NMSstd::vector<int> nms_result;cv::dnn::NMSBoxes(boxes, confidences, _classThreshold, _nmsThreshold, nms_result);std::vector<std::vector<float>> temp_mask_proposals;//cv::Rect holeImgRect(0, 0, srcImg.cols, srcImg.rows);for (int i = 0; i < nms_result.size(); ++i) {int idx = nms_result[i];OutputParams result;result.id = class_ids[idx];result.confidence = confidences[idx];result.rotatedBox = boxes[idx];if (result.rotatedBox.size.width<1|| result.rotatedBox.size.height<1)continue;output.push_back(result);}OutputParams best;if (selectBestDetection(output, srcImg.size(),0, 0.5f, best,distance)) {output.clear();output.push_back(best); // 只保留最佳目标return true;} else {output.clear();return false;}if (output.size())return true;elsereturn false;
}
🔹 成员变量:网络输入尺寸
int _netWidth = 640; // 图片输入宽度
int _netHeight = 640; // 图片输入高度
YOLOv8
模型通常要求输入图像为固定大小(这里是640x640
)。- 在检测前,原图会被缩放到这个尺寸。
🔹 成员变量:类别名称
std::vector<std::string> _className = {"line", "chuizi"};
🔹 私有成员:置信度与 NMS 阈值
private:float _classThreshold = 0.3; // 分类置信度阈值float _nmsThreshold = 0.5; // 非极大值抑制(NMS)阈值
执行旋转目标检测与飞机姿态控制
DJI
(大疆)无人机平台上的一个 RGB
图像回调函数,实现AI
图像处理:
首先每当接收到新图像时,就会执行DjiUser_ShowRgbImageCallback
回调函数,因此能够实时接收相机图像 ,随后 使用 OpenCV + YOLOv8 OBB
模型进行旋转目标检测 ,然后计算目标角度、运动方向、距离, 最后通过消息队列(mq_send
)将结果发送给飞控或其他模块。
static void DjiUser_ShowRgbImageCallback(CameraRGBImage img, void *userData)
{string name = string(reinterpret_cast<char *>(userData));#ifdef OPEN_CV_INSTALLEDMat mat(img.height, img.width, CV_8UC3, img.rawData.data(), img.width * 3);if (s_demoIndex == 3) {int32_t distance = 0;static bool cameraInited = false;T_DjiReturnCode ret = DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS;if (!cameraInited){ret = DjiCameraManager_Init();if (DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS == ret)cameraInited = true;elseprintf("Init camera manager failed, error code: 0x%08X\r\n", ret);}if (ret == DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) {T_DjiCameraManagerLaserRangingInfo laserRangingInfo;T_DjiReturnCode ret = DjiCameraManager_GetLaserRangingInfo(DJI_MOUNT_POSITION_PAYLOAD_PORT_NO1,&laserRangingInfo);if (ret != DJI_ERROR_SYSTEM_MODULE_CODE_SUCCESS) {printf("Get mounted position %d laser ranging info failed", DJI_MOUNT_POSITION_PAYLOAD_PORT_NO1, ret);}else{distance = laserRangingInfo.distance/10;printf("distance is %d.\n", distance);}}// ********************************************执行旋转目标检测***************************************std::vector<OutputParams> result;auto start = std::chrono::high_resolution_clock::now();task_obb_ocv.Detect(mat, net, result,distance);cv::Mat mask = mat.clone();json j;std::map<std::string, std::vector<float>> message;float image_center_x = mat.cols / 2.0f;float image_center_y = mat.rows / 2.0f;// *************************************计算飞行姿态:上升下降,加减速*************************************if(result.size()>0){Movement calc;json boxes_array = json::array();json scores_array = json::array();json class_ids_array = json::array();int i=0;Point2f vertices[4];result[i].rotatedBox.points(vertices); // OpenCV 提供的方法,自动计算四个角点json box_coords = nlohmann::json::array();// 按顺序添加四个角点(默认顺序:左下 -> 左上 -> 右上 -> 右下)for (int k = 0; k < 4; ++k) {box_coords.push_back({vertices[k].x, vertices[k].y});}// 按 "左上、右上、右下、左下" 的顺序排列:json reordered_coords = nlohmann::json::array();reordered_coords.push_back({vertices[1].x, vertices[1].y}); // 左上reordered_coords.push_back({vertices[2].x, vertices[2].y}); // 右上reordered_coords.push_back({vertices[3].x, vertices[3].y}); // 右下reordered_coords.push_back({vertices[0].x, vertices[0].y}); // 左下cv::Point2f ordered[4];order_points(vertices, ordered); // 排序成标准顺序int angle = computeHorizontalAngle(ordered);auto ratios = get_velocity_ratios(angle*10);// 添加到最终数组中for (const auto& point : reordered_coords) {boxes_array.push_back(point);}// 分数scores_array.push_back(0.6f);// 类别名称映射(假设 id=0 表示“导线”)std::string class_name = (result[0].id == 0) ? "导线" : "未知";class_ids_array.push_back(class_name);// 计算移动方向float center_x = result[0].rotatedBox.center.x;float center_y = result[0].rotatedBox.center.y;json movement = calc.move(center_x, center_y, image_center_x, image_center_y);if (distance > 0 && distance < 8) {distance = -1;} else if (distance > 12 && distance < 18) {distance = 1;} else {distance = 0; }// *********************************************数据传输*********************************************j["boxes"] = boxes_array;j["scores"] = scores_array;j["class_ids"] = class_ids_array;j["movement"] = movement;j["distance"] = distance; // 默认值j["hspeed"] = ratios.at("horizontal_ratio");//chuaij["vspeed"] = ratios.at("vertical_ratio");// 把这个对象 发过去std::string dataToBeSent = j.dump(); // 参数4表示缩进空格数(美化格式)// mop 发送消息到消息队列char buffer[1024] = {0};const char *ptr = buffer;// 拷贝字符串内容(不含终止符)std::copy(dataToBeSent.begin(), dataToBeSent.end(), buffer);// 若需要字符串终止符,手动添加buffer[dataToBeSent.size()] = '\0';if (mq_send(s_mqFd, ptr, dataToBeSent.length()+1, 0) == -1) {printf("mq_send error");}printf("%s\n", ptr);printf("Message sent successfully.\n");}elsej["boxes"] = nlohmann::json::array();j["scores"] = nlohmann::json::array();j["class_ids"] = nlohmann::json::array();j["movement"] = {};j["distance"] = 0;j["hspeed"] = 0;j["vspeed"] = 0;std::string dataToBeSent = j.dump(); // 参数4表示缩进空格数(美化格式)// 发送消息到消息队列char buffer[1024] = {0};const char *ptr = buffer;// 拷贝字符串内容(不含终止符)std::copy(dataToBeSent.begin(), dataToBeSent.end(), buffer);// 若需要字符串终止符,手动添加buffer[dataToBeSent.size()] = '\0';if (mq_send(s_mqFd, ptr, dataToBeSent.length()+1, 0) == -1) {printf("mq_send error");}}cv::waitKey(1);#endif
}
这里我们发送数据使用的是mq_send
方法,其是 POSIX
标准中的一个函数,用于向消息队列发送消息。它通常在 <mqueue.h>
头文件中声明。
而如果要实现接收数据,则可以通过 mq_receive
方法实现。
实现效果如下: