源码结构
大疆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<7
std::cout << "OBB Need OpenCV Version >=4.7.0" << std::endl;
return false;
#endif
net = cv::dnn::readNetFromONNX(netPath);
#if CV_VERSION_MAJOR==4 &&CV_VERSION_MINOR==7&&CV_VERSION_REVISION==0
net.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));
// 预处理:blobFromImage
cv::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; // 21504
int net_width = output0.cols; // 20
int class_score_length = net_width - 5; // 15 classes
int angle_index = net_width - 1; // 最后一列是 angle
float* 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;
}
//NMS
std::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;
else
return 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_INSTALLED
Mat 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;
else
printf("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");//chuai
j["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");
}else
j["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
方法实现。
实现效果如下: