C++部署Segment Anything模型:从ONNX转换到推理实现全流程

C++部署Segment Anything模型:从ONNX转换到推理实现全流程 1. 项目概述与核心价值最近在搞一些图像处理相关的项目发现Meta开源的Segment Anything ModelSAM确实是个神器它能对图像中的任何物体进行“一键分割”效果相当惊艳。不过官方主要提供了Python的接口和Demo对于像我这样主要工作在C环境或者需要将模型集成到C原生应用比如一些客户端软件、嵌入式视觉系统或者对Python依赖有洁癖的项目的开发者来说直接调用Python总感觉隔了一层部署起来也麻烦。所以我就琢磨着怎么在纯C环境下把SAM给跑起来。网上相关的资料比较零散要么是讲Python怎么用要么就是C部分一笔带过对于环境配置、依赖处理、模型转换这些关键步骤踩坑的细节很少。这次我就把我从零开始在本地Windows系统上用C成功跑通SAM模型的全过程以及中间遇到的各种“坑”和解决方案毫无保留地分享出来。目标就一个让你照着做一定能成功跑起来。这个“Part1”我们聚焦在最核心的目标上在本地Windows系统上配置好所有环境并运行一个C程序使用ONNX格式的SAM模型完成对一张图片的分割推理。我们会用到ONNX Runtime作为推理引擎OpenCV来处理图像整个流程会涉及模型下载与转换、C工程配置、代码编写与调试。只要你有一点C基础跟着步骤走绝对没问题。2. 环境准备与工具选型在开始写代码之前把“地基”打好至关重要。C项目对环境依赖比较敏感尤其是涉及到深度学习推理时。下面是我经过多次尝试后确定的一套稳定、高效的工具组合。2.1 核心工具清单与安装开发环境Visual Studio 2022为什么选它在Windows上进行C开发VS依然是生态最完善、对Windows特性支持最好的IDE。社区版免费完全够用。它的MSVC编译器、调试器和项目管理系统能极大降低我们配置复杂项目的难度。安装要点安装时在“工作负载”中务必勾选“使用C的桌面开发”。在右侧的“安装详细信息”中确保包含了“用于Windows的C CMake工具”和“MSVC v143 - VS 2022 C x64/x86生成工具”。CMake工具对我们后续编译第三方库很有用。包管理器vcpkg为什么选它管理C的第三方库如OpenCV, ONNX Runtime是件头疼事编译选项、依赖关系、版本冲突能让人崩溃。vcpkg是微软官方的C库管理工具可以自动从源码编译或安装预编译的库并集成到VS中堪称“救命稻草”。安装方法# 打开PowerShell或CMD找一个你喜欢的目录比如 D:\Dev git clone https://github.com/microsoft/vcpkg.git cd vcpkg .\bootstrap-vcpkg.bat # 将vcpkg添加到系统环境变量PATH中方便全局调用 .\vcpkg integrate install推理引擎ONNX Runtime为什么选它SAM的官方PyTorch模型需要转换成ONNX格式才能在C中使用。ONNX Runtime是微软推出的高性能推理引擎对ONNX模型支持最好且提供了简洁的C API。我们通过vcpkg来安装它的C版本。安装命令# 在vcpkg目录下执行 .\vcpkg install onnxruntime:x64-windowsx64-windows表示安装64位Windows版本。安装过程会自动处理所有依赖并编译安装。图像处理库OpenCV为什么选它我们需要用它来加载图片、转换颜色空间、绘制结果等。它是计算机视觉领域的事实标准功能强大同样通过vcpkg安装。安装命令.\vcpkg install opencv4:x64-windows模型与Python环境用于转换Python环境你需要一个Python环境Anaconda或官方Python均可用于运行官方的SAM代码和模型转换脚本。确保安装了PyTorch和SAM相关的库。SAM模型从Meta官方仓库https://github.com/facebookresearch/segment-anything下载模型检查点例如sam_vit_b_01ec64.pthViT-Base模型。我们最终需要的是转换后的ONNX模型。注意版本兼容性是最大的坑我强烈建议你记录下所有工具的版本号。我成功跑通的环境是VS2022 (17.9.7), vcpkg (2024-07-16), ONNX Runtime (1.17.1), OpenCV (4.9.0), PyTorch (2.2.2), SAM (官方main分支)。如果未来版本更新导致问题可以尝试回退到相近版本。2.2 创建Visual Studio项目打开VS2022创建新项目。选择“控制台应用”项目名称比如SegmentAnythingCPPDemo位置自选。解决方案和项目创建好后第一件重要的事是设置项目为x64 Release模式。因为vcpkg默认安装的是x64的库Debug版的库可能需要单独安装为了避免麻烦我们统一用Release。右键项目 - “属性”我们需要配置vcpkg的库路径。C/C - 常规 - 附加包含目录添加你的vcpkg目录下的installed\x64-windows\include。例如D:\Dev\vcpkg\installed\x64-windows\include。链接器 - 常规 - 附加库目录添加installed\x64-windows\lib。例如D:\Dev\vcpkg\installed\x64-windows\lib。链接器 - 输入 - 附加依赖项这里添加我们需要链接的库文件的名字不带路径不带.lib后缀。至少需要onnxruntime.lib opencv_world490.libopencv_world490中的490对应OpenCV 4.9.0版本请根据你实际安装的版本修改。onnxruntime.lib是ONNX Runtime的导入库。3. SAM模型转换从PyTorch到ONNX这是整个流程中最关键、也最容易出错的一步。SAM模型结构比较特殊包含图像编码器Image Encoder和掩码解码器Mask Decoder。官方提供了导出ONNX模型的脚本scripts/export_onnx_model.py但直接使用可能会遇到问题。3.1 标准转换流程准备环境在你的Python环境中确保安装了segment-anything库pip install githttps://github.com/facebookresearch/segment-anything.git以及onnx和onnxruntime。运行转换脚本python scripts/export_onnx_model.py --checkpoint ./sam_vit_b_01ec64.pth --model-type vit_b --output ./sam_onnx_model.onnx这个命令会尝试导出一个包含编码器和解码器的完整模型。但根据我的经验直接导出完整的、动态的解码器ONNX模型在C端用ONNX Runtime加载推理时很容易出现输入输出维度不匹配或运行错误。3.2 更可靠的分离转换法一个更稳定、也是社区更常用的做法是将图像编码器和掩码解码器分开导出为两个独立的ONNX模型。这样结构清晰在C端也更容易控制。导出图像编码器你需要稍微修改一下官方的导出脚本或者使用社区提供的脚本。核心思想是只运行编码器部分将输入图像转换为图像嵌入Image Embedding。这个模型是固定的输入[1, 3, 1024, 1024]的归一化图像输出图像嵌入。导出掩码解码器解码器需要多个输入图像嵌入、点坐标和标签、掩码输入等。导出一个能接受这些动态输入的解码器模型。由于修改脚本涉及代码这里我提供一个经过验证的思路和关键点对于编码器在export_onnx_model.py中找到模型前向传播的部分只保留编码器路径用一个示例输入如torch.randn(1,3,1024,1024)进行torch.onnx.export。对于解码器需要构造解码器需要的所有输入image_embedding, point_coords, point_labels, mask_input, has_mask_input等注意这些输入的维度和类型。point_coords和point_labels需要是动态的第二维可变在导出时使用dynamic_axes参数指定。实操心得我强烈建议你直接寻找社区已经整理好的、可直接导出分离模型的脚本。在GitHub上搜索 “segment anything onnx export separate” 通常能找到。这能节省大量调试时间。我最终使用的是两个独立的ONNX模型sam_encoder.onnx和sam_decoder.onnx。转换后的关键产出sam_encoder.onnx图像编码器模型。sam_decoder.onnx掩码解码器模型。记住编码器输入的图像尺寸要求默认是1024x1024以及需要的预处理归一化到[0,1]再按指定均值和标准差归一化。4. C核心代码实现环境好了模型也有了现在我们来编写C代码。整个过程分为三步加载模型、预处理图像并运行编码器、准备提示信息并运行解码器。4.1 项目结构与头文件在VS项目中我们主要会修改main.cpp。首先包含必要的头文件#include opencv2/opencv.hpp #include onnxruntime_cxx_api.h #include iostream #include vector #include chrono // 定义一些常量方便修改 const int ENCODER_INPUT_SIZE 1024; // SAM编码器默认输入尺寸 const std::string ENCODER_MODEL_PATH “./models/sam_encoder.onnx”; const std::string DECODER_MODEL_PATH “./models/sam_decoder.onnx”; // 图像归一化参数 (来自SAM训练配置) const cv::Scalar MEAN(123.675, 116.28, 103.53); const cv::Scalar STD(58.395, 57.12, 57.375);4.2 初始化ONNX Runtime会话我们需要创建两个Ort::Session分别对应编码器和解码器。Ort::Env env(ORT_LOGGING_LEVEL_WARNING, “SAM_CPP”); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(1); // 设置线程数根据情况调整 session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); // 加载编码器模型 Ort::Session encoder_session(env, ENCODER_MODEL_PATH.c_str(), session_options); // 加载解码器模型 Ort::Session decoder_session(env, DECODER_MODEL_PATH.c_str(), session_options); // 获取模型输入输出信息 auto encoder_input_info encoder_session.GetInputTypeInfo(0); auto encoder_output_info encoder_session.GetOutputTypeInfo(0); // 解码器有多个输入输出需要逐个获取注意SetIntraOpNumThreads控制并行计算的线程数。对于CPU推理设置为0让ORT自己决定或者设置为你的CPU核心数。如果后续发现推理速度慢可以在这里调整。另外如果模型较大可以尝试启用ORT_ENABLE_EXTENDED等优化选项但可能牺牲一些兼容性。4.3 图像预处理与编码器推理这一步的目标是将任意大小的输入图片处理成编码器需要的[1,3,1024,1024]的Tensor。cv::Mat image cv::imread(“./input_image.jpg”); if (image.empty()) { std::cerr “Failed to load image!” std::endl; return -1; } // 1. 调整大小并填充到1024x1024保持长宽比 cv::Mat resized_image; int old_h image.rows, old_w image.cols; float scale std::min(1024.0f / old_w, 1024.0f / old_h); int new_w int(old_w * scale), new_h int(old_h * scale); cv::resize(image, resized_image, cv::Size(new_w, new_h)); cv::Mat padded_image cv::Mat::zeros(ENCODER_INPUT_SIZE, ENCODER_INPUT_SIZE, CV_8UC3); cv::Rect roi(0, 0, new_w, new_h); resized_image.copyTo(padded_image(roi)); // 2. 转换为float并归一化 (这里与Python端预处理保持一致是关键) cv::Mat float_image; padded_image.convertTo(float_image, CV_32FC3, 1.0 / 255.0); // 先转到[0,1] // 按通道减去均值除以标准差 cv::Mat channels[3]; cv::split(float_image, channels); for (int i 0; i 3; i) { channels[i] (channels[i] - MEAN[i]) / STD[i]; } cv::merge(channels, 3, float_image); // 3. 调整维度顺序 HWC - CHW并添加Batch维度 // OpenCV是HWCPyTorch/Tensor是NCHW std::vectorfloat input_tensor_values; input_tensor_values.reserve(1 * 3 * ENCODER_INPUT_SIZE * ENCODER_INPUT_SIZE); for (int c 0; c 3; c) { for (int h 0; h ENCODER_INPUT_SIZE; h) { for (int w 0; w ENCODER_INPUT_SIZE; w) { input_tensor_values.push_back(float_image.atcv::Vec3f(h, w)[c]); } } } // 4. 创建ORT Tensor并运行编码器 std::vectorint64_t input_shape {1, 3, ENCODER_INPUT_SIZE, ENCODER_INPUT_SIZE}; Ort::MemoryInfo memory_info Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat(memory_info, input_tensor_values.data(), input_tensor_values.size(), input_shape.data(), input_shape.size()); auto encoder_output_tensors encoder_session.Run(Ort::RunOptions{nullptr}, encoder_input_names.data(), // 需要提前从session获取输入输出名 input_tensor, 1, encoder_output_names.data(), 1); // 5. 提取图像嵌入 (image_embedding) float* image_embedding_data encoder_output_tensors[0].GetTensorMutableDatafloat(); std::vectorint64_t embedding_shape encoder_output_tensors[0].GetTensorTypeAndShapeInfo().GetShape(); // embedding_shape 应该是 [1, 256, 64, 64] 对于ViT-B模型这里有几个极易出错的细节归一化参数必须和SAM训练时一致MEAN和STD。用错了模型效果会很差。维度顺序OpenCV (HWC) 到 PyTorch/ONNX (NCHW) 的转换是经典坑点顺序错了颜色通道就乱了。填充策略SAM编码器要求固定输入。简单的直接拉伸会变形影响分割效果。这里采用保持长宽比的填充pad填充区域通常用0黑色这需要与模型训练时的处理方式对齐。有些实现会填充一个统一的颜色如114/255的灰色。4.4 准备提示信息与解码器推理假设我们想分割图片中心的一个点。我们需要为解码器准备输入。// 1. 准备点坐标和标签 // 原始图片上的点坐标 (假设是图片中心) int original_point_x old_w / 2; int original_point_y old_h / 2; // 转换到预处理后padded_image的坐标系统 float transformed_x (original_point_x * scale) / ENCODER_INPUT_SIZE; float transformed_y (original_point_y * scale) / ENCODER_INPUT_SIZE; // SAM解码器期望的坐标范围是 [0,1]且是 (x, y) 顺序 std::vectorfloat point_coords {transformed_x, transformed_y}; // 标签1表示前景点0表示背景点。这里我们指定一个前景点。 std::vectorfloat point_labels {1.0f}; // 2. 构建解码器输入 // 解码器通常需要image_embedding, point_coords, point_labels, mask_input, has_mask_input // 我们需要根据导出的解码器ONNX模型的输入顺序和形状来组织数据。 // 假设输入顺序为image_embedding, point_coords, point_labels, mask_input, has_mask_input std::vectorOrt::Value decoder_inputs; // a. image_embedding (来自编码器输出) decoder_inputs.push_back(std::move(encoder_output_tensors[0])); // b. point_coords point_labels // 需要添加一个batch维度并调整形状。例如 point_coords 形状为 [1, 1, 2] std::vectorint64_t point_coords_shape {1, 1, 2}; std::vectorfloat point_coords_data {transformed_x, transformed_y}; Ort::Value point_coords_tensor Ort::Value::CreateTensorfloat(memory_info, point_coords_data.data(), point_coords_data.size(), point_coords_shape.data(), point_coords_shape.size()); decoder_inputs.push_back(std::move(point_coords_tensor)); std::vectorint64_t point_labels_shape {1, 1}; Ort::Value point_labels_tensor Ort::Value::CreateTensorfloat(memory_info, point_labels.data(), point_labels.size(), point_labels_shape.data(), point_labels_shape.size()); decoder_inputs.push_back(std::move(point_labels_tensor)); // c. mask_input (初始掩码全零) std::vectorint64_t mask_input_shape {1, 1, 256, 256}; std::vectorfloat mask_input_data(1*1*256*256, 0.0f); Ort::Value mask_input_tensor Ort::Value::CreateTensorfloat(memory_info, mask_input_data.data(), mask_input_data.size(), mask_input_shape.data(), mask_input_shape.size()); decoder_inputs.push_back(std::move(mask_input_tensor)); // d. has_mask_input (一个标量float类型1.0表示有掩码输入我们这里是初始掩码所以为1.0这里需要根据模型定义来) // 有些模型定义has_mask_input为float1.0或0.0。有些可能是int64。 std::vectorint64_t has_mask_shape {1}; std::vectorfloat has_mask_data {1.0f}; // 或 {0.0f}取决于模型 Ort::Value has_mask_tensor Ort::Value::CreateTensorfloat(memory_info, has_mask_data.data(), has_mask_data.size(), has_mask_shape.data(), has_mask_shape.size()); decoder_inputs.push_back(std::move(has_mask_tensor)); // 3. 运行解码器 auto decoder_output_tensors decoder_session.Run(Ort::RunOptions{nullptr}, decoder_input_names.data(), // 解码器输入名 decoder_inputs.data(), decoder_inputs.size(), decoder_output_names.data(), // 解码器输出名 decoder_output_names.size()); // 4. 解析输出 // 解码器通常输出 masks, scores, low_res_logits // 我们需要 masks (形状可能是 [1, 3, 256, 256]) 和对应的 scores auto masks_tensor decoder_output_tensors[0]; auto scores_tensor decoder_output_tensors[1]; float* masks_data masks_tensor.GetTensorMutableDatafloat(); float* scores_data scores_tensor.GetTensorMutableDatafloat(); auto masks_shape masks_tensor.GetTensorTypeAndShapeInfo().GetShape(); // masks_shape 可能是 [1, 3, 256, 256]表示模型输出了3个候选掩码 int num_masks masks_shape[1]; int mask_size masks_shape[2] * masks_shape[3]; // 256 * 256 // 选择分数最高的掩码 int best_mask_idx 0; float best_score scores_data[0]; for (int i 1; i num_masks; i) { if (scores_data[i] best_score) { best_score scores_data[i]; best_mask_idx i; } } // 提取最佳掩码数据 std::vectorfloat best_mask_data(mask_size); for (int i 0; i mask_size; i) { best_mask_data[i] masks_data[best_mask_idx * mask_size i]; }坐标转换的逻辑是重中之重也是最容易晕的地方用户给的坐标是相对于原始图片的。图片经过缩放和填充变成了1024x1024。我们需要将原始坐标映射到这个预处理后的坐标系中。映射后的坐标还需要归一化到[0, 1]区间这才是SAM解码器期望的输入。顺序是(x, y)不是(行, 列)。4.5 后处理与结果可视化解码器输出的掩码是256x256的低分辨率掩码并且值在(-∞, ∞)范围logits。我们需要将其上采样到原始图像大小并转换为二值掩码。// 1. 将低分辨率掩码 (256x256) reshape 并上采样到原始图片大小 (old_h, old_w) cv::Mat low_res_mask(256, 256, CV_32FC1, best_mask_data.data()); cv::Mat resized_mask; // 上采样到预处理后的尺寸 (new_h, new_w)注意这里不是直接到old_h, old_w cv::resize(low_res_mask, resized_mask, cv::Size(new_w, new_h), 0, 0, cv::INTER_LINEAR); // 2. 将logits通过sigmoid转换为概率并二值化 (阈值通常取0.0因为logits0对应概率0.5) cv::Mat prob_mask; cv::exp(-resized_mask, prob_mask); // 先计算 exp(-x) prob_mask 1.0 / (1.0 prob_mask); // sigmoid(x) 1/(1exp(-x)) cv::Mat binary_mask prob_mask 0.5; // 阈值化 // 3. 由于我们之前填充了黑边需要将掩码裁剪到有效图像区域 (roi)并缩放到原始图像大小 cv::Mat final_mask cv::Mat::zeros(old_h, old_w, CV_8UC1); cv::Mat roi_mask binary_mask(cv::Rect(0, 0, new_w, new_h)); // 裁剪出非填充区域 cv::resize(roi_mask, final_mask, cv::Size(old_w, old_h), 0, 0, cv::INTER_NEAREST); // 用最近邻插值保持二值性 // 4. 可视化将掩码叠加到原图上 cv::Mat colored_mask(old_h, old_w, CV_8UC3, cv::Scalar(0, 255, 0)); // 绿色掩码 cv::Mat result_image image.clone(); colored_mask.copyTo(result_image, final_mask); // 仅在有掩码的区域着色 cv::imshow(“Segmentation Result”, result_image); cv::waitKey(0); cv::imwrite(“./output_result.jpg”, result_image);实操心得掩码上采样到原始尺寸时插值方法的选择有讲究。从256x256上采样到new_h x new_w可以用线性插值(INTER_LINEAR)。但从new_h x new_w缩放到最终的old_h x old_w时由于已经是二值掩码了最好用最近邻插值(INTER_NEAREST)避免边缘产生模糊的灰度值。另外sigmoid操作也可以直接用cv::compare(resized_mask, 0.0, binary_mask, cv::CMP_GT)因为logits0即概率0.5。5. 编译、运行与问题排查代码写完了最后一步就是编译运行。5.1 编译配置与运行确保依赖库路径正确如前所述在项目属性中正确设置了包含目录、库目录和附加依赖项。将模型文件放入正确位置将转换好的sam_encoder.onnx和sam_decoder.onnx放在项目目录下的models文件夹中或者你在代码中指定的路径。准备输入图片将一张测试图片如input_image.jpg放在项目目录下。编译运行在VS中按CtrlF5开始执行不调试或F5调试。如果一切配置正确程序会运行并弹窗显示分割结果。5.2 常见问题与解决方案实录在实际操作中你几乎一定会遇到下面这些问题。我把我的踩坑记录分享出来问题1编译时链接错误提示无法打开包括文件: “onnxruntime_cxx_api.h”或无法解析的外部符号。原因这是最常见的环境配置问题。vcpkg安装的库路径没有正确添加到VS项目中。排查检查项目属性中的“附加包含目录”和“附加库目录”路径是否正确指向了vcpkg\installed\x64-windows\include和lib。检查“附加依赖项”中的库文件名是否正确特别是OpenCV的版本号如opencv_world490.lib。终极解决方案使用vcpkg的集成命令。在vcpkg目录下运行.\vcpkg integrate project它会为你的VS解决方案生成一个属性表。在你的VS解决方案资源管理器中右键解决方案 - “添加” - “现有项”找到生成的vcpkg.props文件并添加。这能确保所有vcpkg安装的库都被正确引用。问题2运行时崩溃错误信息关于Ort::Session初始化失败或Run失败。原因AONNX模型文件路径错误或模型损坏。解决检查模型路径是否正确并用Netron一个神经网络可视化工具打开你的.onnx文件确认模型结构正常并记下输入输出的名称和形状与你代码中的encoder_input_names等变量核对。输入输出名称必须完全一致包括大小写。原因B输入Tensor的形状或数据类型与模型期望不匹配。解决这是最可能的原因。仔细核对编码器输入的shape是否为[1,3,1024,1024]数据类型是否为float。核对解码器每个输入的shape和类型。使用GetTensorTypeAndShapeInfo()打印模型期望的shape进行对比。原因C解码器动态轴设置问题。解决如果你导出的解码器模型支持动态点坐标第二维可变在C端创建Tensor时point_coords_shape应为{1, num_points, 2}其中num_points是你实际提供的点数。确保与导出模型时指定的dynamic_axes一致。问题3分割结果不对要么全图都是掩码要么什么都没有。原因A图像预处理错误。解决99%的问题出在这里逐行检查你的预处理代码BGR转RGB做了吗SAM训练用的是RGBOpenCV默认加载是BGR。归一化参数MEAN和STD用对了吗是(123.675/255, 116.28/255, 103.53/255)这样的值吗维度顺序HWC-CHW转换正确吗建议将预处理后的第一个像素值打印出来与用Python脚本处理同一张图片的结果进行对比必须完全一致。原因B点坐标转换错误。解决在代码中打印出original_point_x/y,transformed_x/y的值。确保缩放因子scale计算正确。确保最终传给解码器的坐标在[0,1]范围内。可以尝试用一个非常明显的位置比如图片左上角(10,10)进行测试。原因C掩码后处理阈值问题。解决解码器输出的是logits。尝试调整二值化的阈值不一定非要是0.0。可以先将logits通过sigmoid得到概率图保存下来看看概率分布再决定阈值。问题4推理速度很慢。原因在CPU上运行ViT模型本身就不快。优化使用更小的SAM模型如vit_t(Tiny) 或vit_s(Small)。在session_options中尝试设置SetIntraOpNumThreads为你的CPU核心数。如果支持尝试使用ONNX Runtime的CUDA或TensorRT执行提供者需要安装对应版本的ONNX Runtime并配置。这需要GPU和额外的环境配置是下一步优化的方向。问题5如何支持多点提示正负点或框提示原理解码器的point_coords和point_labels输入支持多个点。只需将多个点的坐标和标签1前景0背景拼接起来即可。例如两个点point_coords {x1, y1, x2, y2}point_labels {1.0, 0.0}。对应的Tensor形状应为[1, 2, 2]。框提示一个框可以用两个点表示左上角和右下角并将这两个点的标签都设为2在SAM中标签2通常代表框。具体需要查阅SAM的原始代码确定标签定义。然后在坐标数组中传入四个值[x1, y1, x2, y2]标签数组为[2, 2]。同样形状要调整为[1, 2, 2]。整个流程走下来虽然步骤不少但每一步拆解开后都是有逻辑可循的。最花时间的往往不是写C代码而是模型转换和环境配置。一旦本地成功跑通你就拥有了在C项目中集成最强零样本分割能力的基础。接下来你可以考虑封装成类、优化性能、集成交互式点选功能等等那就是Part2及以后的故事了。