1. 项目概述:为什么选择C++ API来驾驭OpenPose?
如果你已经用OpenPose的命令行工具跑通了几个Demo,看着屏幕上实时渲染出的人体骨骼点,心里可能正盘算着:“这玩意儿怎么集成到我自己的项目里?” 或者,你发现命令行参数调来调去,始终无法满足你对输入源、输出格式或后处理的定制需求。这时,官方文档里那个“C++ API”的选项,就从一行不起眼的文字,变成了你必须要啃下来的硬骨头。
OpenPose的C++ API,本质上是一套供开发者直接调用的底层接口库。它把命令行工具背后那套复杂的姿态估计流水线——从图像读取、神经网络推理、关键点检测到渲染输出——全部封装成了一个个你可以直接操纵的C++类和函数。这意味着,你不再被--video、--write_json这些预设好的命令行参数所束缚。你可以从任何地方读取图像(比如工业相机、内存缓冲区、网络流),可以把检测到的关节点数据(std::vector<std::vector<Datum>>)拿过来进行二次分析(比如计算关节角度、识别特定动作),也可以把结果用任何你喜欢的方式呈现或保存(比如叠加到3D场景、发送到另一个服务)。
我选择深入C++ API,是因为在几个实际项目中遇到了天花板。一个是在嵌入式设备上做实时动作分析,需要极致的性能控制,命令行工具的开销和灵活性都不够。另一个是需要将姿态数据与自研的算法模块深度耦合,Python API虽然易用,但在复杂数据流处理和延迟上不如C++直接。如果你也面临类似的需求:高性能集成、深度定制、以及对整个处理流程的完全掌控,那么直接使用C++ API几乎是唯一的选择。当然,这条路对C++功底和工程能力有一定要求,但回报是彻底摆脱“黑盒”,获得真正的灵活性。
2. 环境准备与项目配置:从零搭建可编译的OpenPose C++开发环境
在兴奋地打开IDE准备写代码之前,一个稳定、可编译的C++开发环境是重中之重。很多人在这里踩坑,不是因为OpenPose本身复杂,而是环境依赖没理顺。
2.1 基础依赖安装与验证
OpenPose的C++开发强烈依赖CMake进行项目构建。首先,确保你的系统有足够新的CMake(>=3.12)、GCC/G++(支持C++11)或Visual Studio(2017以上)。在Ubuntu上,可以这样安装:
sudo apt-get update sudo apt-get install cmake g++ git接下来是OpenPose的核心依赖:CUDA(用于GPU加速)、cuDNN、OpenCV。我的经验是,严格遵循版本匹配。OpenPose 1.7.0官方推荐CUDA 10.x或11.x,cuDNN对应版本,以及OpenCV 3.x或4.x。我曾经在CUDA 11.4上折腾了半天,最后发现某个算子不支持,退回11.2才一切正常。安装后,务必验证:
# 验证CUDA nvcc --version # 验证OpenCV(进入Python环境) python -c “import cv2; print(cv2.__version__)”注意:如果你主要在CPU模式下开发测试,可以不装CUDA/cuDNN,但编译时务必加上
-DGPU_MODE=CPU_ONLY的CMake选项,否则编译会失败。不过,CPU模式的速度会慢很多,仅建议用于功能验证。
2.2 源码获取与CMake编译实战
直接从GitHub克隆官方仓库,并切换到稳定的发布分支(如tag v1.7.0),避免使用可能不稳定的master分支。
git clone https://github.com/CMU-Perceptual-Computing-Lab/openpose.git cd openpose git checkout v1.7.0编译是第一个真正的挑战。我建议在项目根目录下创建一个独立的构建目录,保持源码树的干净。
mkdir build && cd build关键的CMake配置命令来了。这里有几个决定后续开发体验的选项:
cmake .. -DBUILD_EXAMPLES=ON -DBUILD_SHARED_LIBS=ON -DDOWNLOAD_BODY_COCO_MODEL=ON -DDOWNLOAD_HAND_MODEL=ON让我解释一下这几个参数:
-DBUILD_EXAMPLES=ON:这会把examples/目录下的所有C++示例程序都编译出来。对我们来说,这是最重要的参考代码库,必须打开。-DBUILD_SHARED_LIBS=ON:生成动态链接库(.so或.dll)。这样,在你自己的应用程序中,只需要链接这些库文件,而不必每次都重新编译整个OpenPose。对于开发来说更方便。-DDOWNLOAD_*_MODEL=ON:让CMake在编译时自动下载预训练模型文件。强烈建议打开,否则你需要手动寻找并放置模型,非常容易出错。
配置完成后,就是漫长的编译过程:
make -j`nproc` # 在Linux上,使用所有CPU核心加速编译 # 或者在Windows上,用CMake打开生成的.sln文件,在Visual Studio中编译编译成功与否,是环境是否就绪的终极检验。如果失败,通常的错误信息会指向缺失的依赖库或版本冲突。此时,需要仔细核对错误日志,并回到上一步检查依赖。
2.3 第一个验证:运行C++示例程序
编译成功后,在build/examples/tutorial_api_cpp/目录下,你会找到生成的可执行文件,例如01_body_from_image_default.bin。运行它,并指定一个输入图像:
./examples/tutorial_api_cpp/01_body_from_image_default.bin --image_dir ../examples/media/ --write_images ./output/如果一切顺利,你会在./output/目录下看到处理后的图像,上面画出了人体的骨骼关键点。这一步至关重要,它不仅验证了编译是否真正成功,还确认了你的运行时环境(尤其是GPU驱动和CUDA库)配置正确。只有示例程序能跑通,你后续基于API的自开发程序才有成功的基础。
3. C++ API核心架构与关键类深度解析
跑通示例只是开始,要自己写代码,必须理解OpenPose C++ API里几个核心的“积木”。这套API的设计遵循了经典的生产者-消费者流水线模式,理解这个模式,代码读起来就顺畅了。
3.1 核心类:op::Wrapper与op::Datum
整个OpenPose处理流程的“总指挥”是op::WrapperT<Datum>这个模板类。你几乎所有的操作都围绕它展开。它的主要职责是管理整个姿态估计的流水线:配置参数、启动工作线程、管理数据流入流出。你可以把它想象成一个高度可配置的工厂流水线控制器。
而在这条流水线上流动的“产品”,就是op::Datum这个数据结构。这是整个API数据交换的核心,你必须吃透它。一个Datum对象在一次处理流程中,包含了从输入到输出的所有数据:
struct Datum { // 输入 cv::Mat cvInputData; // 输入的原始图像 std::vector<cv::Mat> cvInputData3D; // 3D输入(如果有) // 输出 std::vector<op::Rectangle<float>> poseRectangles; // 检测到的人体矩形框 std::vector<std::vector<op::Point<float>>> poseKeypoints; // 关键点坐标,核心数据! std::vector<cv::Mat> poseHeatMaps; // 热图(如果启用) std::vector<cv::Mat> poseCandidates; // 候选点(如果启用) // 其他输出(手、脸、3D等) std::vector<std::vector<std::vector<op::Point<float>>>> handKeypoints; std::vector<std::vector<op::Point<float>>> faceKeypoints; // 渲染后的图像 cv::Mat cvOutputData; // 其他元数据 unsigned long long id; // 数据ID // ... 还有其他成员 };对我们开发者来说,最常打交道的就是cvInputData(送入图像)和poseKeypoints(取出关键点)。poseKeypoints是一个三维向量:[人物索引][关键点索引][x, y, 置信度]。例如,poseKeypoints[0][0][0]就是第一个人物鼻子关键点的x坐标。
3.2 配置系统:op::WrapperStructPose与标志位
在命令行中你用--net_resolution 656x368,在C++ API里,你需要通过配置结构体来设置。op::WrapperStructPose包含了所有身体姿态估计相关的参数:
op::WrapperStructPose poseConfig; poseConfig.netInputSize = op::Point<int>(656, 368); // 网络输入尺寸 poseConfig.outputSize = op::Point<int>(1280, 720); // 输出图像尺寸 poseConfig.numScales = 1; // 尺度数 poseConfig.scaleGap = 0.25; // 尺度间隔 poseConfig.renderMode = op::RenderMode::Cpu; // 渲染模式(Cpu, Gpu) // 是否启用特定部件 poseConfig.enable = true; poseConfig.disableBlending = false; // 是否禁用渲染混合 // 模型文件夹路径,至关重要! poseConfig.modelFolder = “../models/”;类似地,还有WrapperStructHand(手部)、WrapperStructFace(面部)、WrapperStructExtra(额外功能如3D)、WrapperStructInput(输入)、WrapperStructOutput(输出)等。你需要创建这些配置对象,设置好参数,然后传给Wrapper对象。
这里有一个极易踩坑的点:modelFolder的路径。你必须确保这个路径指向正确的模型文件目录(包含pose/body25/、pose/coco/、hand/等子目录)。如果路径错误,Wrapper在初始化时会静默失败或抛出难以理解的异常。我习惯使用绝对路径来避免歧义。
3.3 数据流模式:生产者、消费者与回调函数
Wrapper类通过emplaceAndPop()或waitAndPop()方法来处理数据,但这属于较底层的用法。对于大多数应用,更高效的方式是配置生产者(Producer)和消费者(Consumer)。
- 生产者:负责产生
Datum数据。可以是视频文件、图像目录、摄像头或自定义来源。通过WrapperStructInput进行配置。 - 消费者:负责处理
Wrapper处理后的Datum结果。可以是显示窗口、文件保存器或你自己的自定义函数。
最强大的功能是自定义消费者回调。你可以注册一个函数,每当一帧数据被处理完成后,这个函数就会被调用,并接收到包含所有结果的Datum对象。这是你注入自定义逻辑(如动作识别、数据转发)的入口。
void myUserPostProcessingCallback(const std::shared_ptr<std::vector<op::Datum>>& datumsPtr) { if (datumsPtr != nullptr && !datumsPtr->empty()) { const auto& datum = datumsPtr->at(0); // 1. 在这里访问处理结果 const auto& poseKeypoints = datum.poseKeypoints; if (!poseKeypoints.empty()) { // 2. 你的自定义逻辑:比如计算第一个人物的右肘角度 const auto& neck = poseKeypoints[0][1]; // 脖子关键点索引可能为1 const auto& rShoulder = poseKeypoints[0][2]; const auto& rElbow = poseKeypoints[0][3]; // ... 计算角度 ... } // 3. 或者将数据发送到网络、写入数据库等 // sendToNetwork(datum); } }理解并熟练运用生产者-消费者模式和回调函数,是你从“能调用API”到“能优雅地集成API”的关键一步。
4. 实战:构建你的第一个自定义OpenPose C++应用
理论说得再多,不如动手写一行代码。让我们抛开示例,从头创建一个最简单的自定义应用,它的功能是:读取摄像头视频流,检测人体姿态,并在控制台实时打印出第一个检测到人物的鼻子关键点坐标。
4.1 项目文件结构与CMake集成
首先,在OpenPose源码的examples/user_code/目录下(官方推荐的自定义代码区),创建我们的项目文件my_pose_app.cpp。然后,我们需要修改同一目录下的CMakeLists.txt文件,将我们的应用添加进去,这样它就能随着OpenPose一起编译了。
找到examples/user_code/CMakeLists.txt,在末尾添加:
# 添加我们自定义的可执行文件 add_executable(my_pose_app my_pose_app.cpp) # 将我们的可执行文件链接到OpenPose的核心库 target_link_libraries(my_pose_app ${OpenPose_LIBS} ${CMAKE_THREAD_LIBS_INIT}) # 设置输出目录,方便查找 set_target_properties(my_pose_app PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/examples/user_code)保存后,回到OpenPose的build目录,重新运行cmake ..(配置会更新),然后再次执行make编译。你会发现my_pose_app也被编译出来了,位于build/examples/user_code/下。
4.2 核心代码编写:初始化、配置与运行循环
现在,打开my_pose_app.cpp,开始编写代码。
第一步:包含必要的头文件并配置命名空间。
// 引入OpenPose的核心头文件 #include <openpose/headers.hpp> // 引入OpenCV,用于图像显示(如果需要) // #include <opencv2/highgui/highgui.hpp> // 使用OpenPose的命名空间,简化代码 namespace op = openpose; int main(int argc, char** argv) { try { // 第二步:初始化Wrapper配置 op::Wrapper opWrapper{op::ThreadManagerMode::Asynchronous}; // 使用Asynchronous模式,避免处理线程阻塞生产者。第二步:配置各个模块的参数。这是代码的核心部分,决定了OpenPose的行为。
// 1. 配置姿态估计 op::WrapperStructPose poseConfig; poseConfig.netInputSize = op::Point<int>(368, 368); // 较小的分辨率,速度更快 poseConfig.modelFolder = “../models/”; // 模型路径,根据你的实际安装位置调整 poseConfig.enable = true; poseConfig.disableBlending = true; // 为了纯数据输出,先禁用渲染混合 // 2. 配置手部和面部(本例中先禁用,以简化) op::WrapperStructHand handConfig; handConfig.enable = false; op::WrapperStructFace faceConfig; faceConfig.enable = false; // 3. 配置输入源:使用默认摄像头(索引0) op::WrapperStructInput inputConfig; inputConfig.producerType = op::ProducerType::Webcam; inputConfig.producerString = “0”; // 摄像头设备索引 // 4. 配置输出:我们暂时不在GUI显示,只做控制台输出 op::WrapperStructOutput outputConfig; outputConfig.displayMode = op::DisplayMode::NoDisplay; // 不显示GUI窗口 // outputConfig.writeImages = “./output/“; // 如果需要保存图片,可以设置路径第三步:组装配置并启动Wrapper。
// 将配置应用到Wrapper opWrapper.configure(poseConfig); opWrapper.configure(handConfig); opWrapper.configure(faceConfig); opWrapper.configure(inputConfig); opWrapper.configure(outputConfig); // 第四步:启动Wrapper,开始工作线程 opWrapper.start();第四步:主循环——获取并处理数据。这里我们使用emplaceAndPop模式,手动向流水线送入数据(虽然我们配置了摄像头生产者,但这里演示另一种更可控的方式)。
// 为了演示,我们改用手动循环从摄像头抓帧并处理 // 实际上,配置了Webcam生产者后,Wrapper内部会自动循环。 // 这里我们演示如何手动控制流程(例如处理特定帧数)。 const int maxFrames = 100; // 处理100帧后退出 int frameCount = 0; auto cap = cv::VideoCapture(0); // 手动打开摄像头 while (frameCount < maxFrames && cap.isOpened()) { cv::Mat cvImage; cap >> cvImage; // 捕获一帧 if (cvImage.empty()) break; // 创建一个Datum对象并填充图像数据 auto datumProcessed = std::make_shared<std::vector<op::Datum>>(); datumProcessed->emplace_back(); auto& datum = datumProcessed->at(0); datum.cvInputData = cvImage; // 将Datum送入OpenPose流水线进行处理 bool successfullyEmplaced = opWrapper.emplaceAndPop(datumProcessed); if (successfullyEmplaced && !datumProcessed->empty()) { const auto& resultDatum = datumProcessed->at(0); // 检查并输出关键点 if (!resultDatum.poseKeypoints.empty()) { const auto& keypoints = resultDatum.poseKeypoints[0]; // 第一个人的关键点 // 假设鼻子关键点的索引是0(根据COCO模型) const auto& nose = keypoints[0]; float x = nose[0]; float y = nose[1]; float confidence = nose[2]; if (confidence > 0.2) // 设置一个置信度阈值 { std::cout << “Frame “ << frameCount << “: Nose at (“ << x << “, “ << y << “), conf=” << confidence << std::endl; } } } else { std::cerr << “Failed to process frame “ << frameCount << std::endl; } frameCount++; } cap.release(); std::cout << “Finished processing “ << frameCount << “ frames.” << std::endl; } catch (const std::exception& e) { std::cerr << “Error: “ << e.what() << std::endl; return -1; } return 0; }4.3 编译、运行与调试
代码写完后,在build目录下执行make进行编译。编译成功后,在build/examples/user_code/目录下运行生成的可执行文件:
./examples/user_code/my_pose_app如果一切正常,你应该能看到控制台不断打印出摄像头画面中检测到的人体鼻子坐标。恭喜你,你已经成功使用OpenPose C++ API创建了第一个自定义应用!这个简单的例子包含了配置、初始化、数据传递和结果提取的全流程。你可以在此基础上,轻松地修改输入源(比如换成视频文件或图像列表)、增加手部和面部检测、或者将输出的关键点数据用于更复杂的业务逻辑。
5. 高级应用与性能优化技巧
当你的基本应用跑通后,下一步就是让它更健壮、更高效、更能满足实际项目需求。这部分分享的是一些在实战中积累下来的高级技巧和避坑指南。
5.1 处理多人物与复杂场景
在拥挤的场景中,poseKeypoints向量里会包含多个人物的数据。你需要遍历所有人,并为每个人物执行你的逻辑。
if (!resultDatum.poseKeypoints.empty()) { int personCount = resultDatum.poseKeypoints.size(); for (int personIdx = 0; personIdx < personCount; ++personIdx) { const auto& keypointsOfPerson = resultDatum.poseKeypoints[personIdx]; // 计算该人物的边界框(近似) float minX = 1e9, maxX = 0, minY = 1e9, maxY = 0; int validKptCount = 0; for (const auto& kpt : keypointsOfPerson) { if (kpt[2] > 0.05) { // 置信度阈值 minX = std::min(minX, kpt[0]); maxX = std::max(maxX, kpt[0]); minY = std::min(minY, kpt[1]); maxY = std::max(maxY, kpt[1]); validKptCount++; } } if (validKptCount > 5) { // 至少有5个有效关键点才认为是一个有效人物 std::cout << “Person “ << personIdx << “ bbox: [“ << minX << “, “ << minY << “, “ << maxX << “, “ << maxY << “]” << std::endl; } } }对于遮挡严重或非常密集的人群,OpenPose的检测可能会不稳定,出现关键点闪烁或身份交换(ID Switch)。在要求高的场景(如动作分析),可能需要引入跟踪算法(如SORT, DeepSORT)来维持人物ID的连续性。
5.2 自定义输入与输出:超越摄像头和图像
WrapperStructInput支持多种生产者类型(ProducerType):
ImageDirectory:处理一个目录下的所有图片。Video:处理视频文件。Webcam:摄像头。IPCamera:网络摄像头RTSP流。FlirCamera:FLIR热像仪。None:无生产者,用于纯手动emplace数据。
处理网络流或内存数据是一个常见需求。你可以选择ProducerType::None,然后自己管理图像捕获(比如用libcurl获取图片,或用FFmpeg解码网络流),将得到的cv::Mat填充到Datum.cvInputData,再调用opWrapper.emplaceAndPop(datumProcessed)。
对于输出,除了保存图像和JSON,你可以在自定义消费者回调函数里做任何事情:
- 实时网络传输:将
poseKeypoints序列化为Protobuf或JSON,通过WebSocket或gRPC发送给前端或其他服务。 - 数据库存储:将关键点数据和时间戳写入MySQL、PostgreSQL或时序数据库。
- 触发事件:根据特定的姿态组合(如举手、跳跃)触发业务逻辑。
5.3 性能调优与资源管理
OpenPose是计算密集型应用,性能优化至关重要。
- 网络分辨率:
netInputSize是最大的性能杠杆。656x368是精度和速度的平衡点。对于实时视频,368x368甚至320x240可以大幅提升FPS,但会损失对小目标或远处人物的检测精度。你需要根据实际场景做权衡。 - 尺度数量:
numScales和scaleGap用于多尺度检测以提升精度,但会线性增加计算量。对于固定场景(如摄像头高度不变),通常numScales = 1就足够了。 - 渲染开销:
op::RenderMode::Cpu和op::RenderMode::Gpu。如果不需要可视化输出,务必设置disableBlending = true并关闭显示(DisplayMode::NoDisplay),这能节省大量CPU/GPU时间。如果需要渲染,在GPU允许的情况下,使用Gpu模式通常更快。 - 批处理(Batch Processing):对于图像目录处理,OpenPose内部会尝试批处理以提高GPU利用率。确保你的
WrapperStructPose配置一致。 - 内存管理:长时间运行的应用要注意内存泄漏。确保你的
Datum对象被正确管理,避免在回调函数中创建大量临时对象。使用智能指针(std::shared_ptr)可以帮助管理生命周期。 - 线程模式:
op::ThreadManagerMode::Asynchronous(异步)是默认推荐模式,生产者、处理者、消费者各司其职,效率最高。Synchronous(同步)模式更易于调试,但性能较差。
一个常见的性能瓶颈是数据在CPU和GPU之间的拷贝。如果你需要将处理后的图像(cvOutputData)用于其他GPU计算(如另一个深度学习模型),尽量让整个流水线保持在GPU内存中,避免不必要的cudaMemcpy。这可能需要修改OpenPose源码或使用更底层的接口,属于高级优化范畴。
6. 常见问题排查与调试心得
即使按照教程一步步来,也难免会遇到各种“妖孽”问题。这里把我踩过的坑和解决方法汇总一下,希望能帮你快速排雷。
6.1 编译与链接问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
fatal error: openpose/headers.hpp: No such file or directory | 编译器找不到OpenPose头文件。 | 确保你的CMakeLists.txt正确包含了OpenPose的头文件路径。在自定义项目的CMakeLists.txt中添加include_directories(${OpenPose_INCLUDE_DIRS})。 |
undefined reference toop::WrapperT op::Datum ::WrapperT(...)` | 链接错误,找不到OpenPose库。 | 确保target_link_libraries(your_app ${OpenPose_LIBS})语句正确,且OpenPose已成功编译为共享库(BUILD_SHARED_LIBS=ON)。 |
运行时错误:error while loading shared libraries: libopenpose.so.1.7.0: cannot open shared object file | 系统运行时找不到动态库。 | 将OpenPose的库路径(如/path/to/openpose/build/lib)添加到LD_LIBRARY_PATH环境变量中,或者将.so文件复制到系统库目录。 |
6.2 运行时与逻辑错误
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 程序启动后立即崩溃或无输出。 | 模型路径modelFolder设置错误。 | 使用绝对路径,并确认该路径下存在pose/body25/pose_deploy.prototxt、pose/body25/pose_iter_584000.caffemodel等模型文件。 |
关键点poseKeypoints始终为空。 | 1. 输入图像为空或格式异常。 2. 网络分辨率设置不当,目标太小。 3. GPU内存不足,模型未加载。 | 1. 检查cvInputData是否成功载入有效图像。2. 尝试增大 netInputSize,或对输入图像进行缩放。3. 查看日志,尝试在CPU模式下运行( -DGPU_MODE=CPU_ONLY)以排除GPU问题。 |
| 检测结果抖动严重,关键点坐标跳变。 | 1. 视频流帧率过高,处理跟不上。 2. 光照剧烈变化或背景复杂。 3. 单帧检测本身的不稳定性。 | 1. 在WrapperStructInput中设置producerFpsMode为OriginalFps,或对输入进行跳帧。2. 对输入图像进行预处理(如直方图均衡化、高斯模糊)。 3. 引入滤波算法(如卡尔曼滤波、一阶低通滤波)对关键点序列进行平滑。 |
| 内存使用量随时间不断增加。 | 自定义回调函数或主循环中持续分配内存未释放。 | 使用Valgrind或AddressSanitizer检查内存泄漏。确保没有在循环内无节制地创建大型临时对象。对于cv::Mat,注意其引用计数和手动释放。 |
6.3 调试技巧
- 启用日志:在
Wrapper启动前,调用op::ConfigureLog::setPriorityThreshold(op::Priority::High);可以设置日志级别。Debug级别会输出大量信息,有助于定位问题。 - 逐步验证:不要一次性写完全部逻辑。先确保能正确读取输入,再确保
Wrapper能初始化,接着验证是否能拿到非空的Datum,最后才处理关键点数据。 - 可视化中间结果:即使最终输出不需要图像,在调试阶段,可以临时启用
outputConfig.displayMode = op::DisplayMode::Display2D并设置一个合理的render_pose参数,将渲染后的图像显示出来,直观地判断检测是否正常。 - 比对命令行版本:当你的C++ API程序行为异常时,用相同的输入图像和近似参数运行命令行Demo (
build/examples/openpose/openpose.bin)。如果命令行正常而你的程序不正常,问题大概率出在你的配置或数据传递逻辑上。
最后,一个最朴素的建议:仔细阅读官方示例代码。examples/tutorial_api_cpp/目录下的几个.cpp文件几乎涵盖了所有基础用法。从最简单的例子开始,一行行理解,比任何教程都管用。当你理解了数据如何在Datum中流动,如何通过Wrapper配置整个系统,你就真正掌握了OpenPose C++ API的精髓,可以自由地将其嵌入到任何你想要的C++项目中了。