C# 接口使用说明#

本页集中说明 VisionFlow C++ 原生接口与 C# 绑定不一致 的地方,供 C# 开发者查阅。 环境配置见 在开发环境中引入VisionFlow;本页只讲差异,不是完整 API 参考。

Note

C# 绑定由 SWIG 从原生 visionflow:: 接口生成,覆盖多数接口。具体成员是否存在、 是字段还是属性、是否可 Dispose,一律以你所用发布包中实际生成的 visionflow.cs 为准。

运行时文件与生成文件#

visionflow.cs 只是 C# 侧的托管包装代码,不能单独运行。编译 C# 项目时,还需要 同时部署与其匹配的 native 包装库 wrapper_csharp_visionflow.dll 以及 VisionFlow 本体和算法模块所需的其他 native DLL。进程位数也必须匹配(x64)。

C# 程序集
    ↓ DllImport("wrapper_csharp_visionflow")
wrapper_csharp_visionflow.dll
    ↓
VisionFlow native DLL 及算法模块

常见加载问题包括:

  • DllNotFoundExceptionwrapper_csharp_visionflow.dll 或其依赖库不在 native DLL 搜索路径中;

  • BadImageFormatException:通常是 x86/x64 位数不匹配,也可能是 DLL 格式无效 或文件损坏;

  • EntryPointNotFoundException:native DLL 已找到,但不包含当前 C# 包装代码 所需的导出入口;常见原因是 C# 包装文件与 native DLL 的生成配置或导出入口不匹配。

visionflow.cs 是自动生成文件,不应直接修改。

命名空间与符号映射#

  • 命名空间:C++ visionflow:: → C# visionflow.;子命名空间同名对应,如 visionflow::propsvisionflow.propsvisionflow::imgvisionflow.img

  • STL 容器:std:: 落到 C# 的 std 命名空间。

  • 自由函数(非成员函数):不再有裸函数,按所属模块挂到某个 visionflow_*_global 静态类下—— 子命名空间多与其同名(visionflow::imgvisionflow_img_global),顶层 visionflow:: 的函数则落在 visionflow_globalvisionflow_core_global``(如 ``adapt)。

// 初始化等全局自由函数 → visionflow_global
visionflow_global.initialize(opts);

// 各命名空间的自由函数 → 对应的 visionflow_*_global 静态类
visionflow_img_global.draw(image, rect, color, -1);                        // visionflow::img
visionflow_helper_global.add_image_to_sample(sample, image, input_id);     // visionflow::helper
// 顶层 visionflow::adapt 落在 core 模块类 visionflow_core_global:
var adapter  = visionflow_core_global.adapt(project, configurator);

// STL 容器 → std 命名空间
var ids = new std.VectorInt { 100, 10000 };
var modules = new std.VectorString { "Segmentation", "Input" };
var kv = new std.MapStringString();

下表为常用映射:

C++

C#

visionflow::initialize

visionflow_global.initialize

visionflow::adapt

visionflow_core_global.adapt

visionflow::helper::add_image_to_sample

visionflow_helper_global.add_image_to_sample

visionflow::props::PolygonRegionList

visionflow.props.PolygonRegionList

std::vector<int> / std::vector<std::string>

std.VectorInt / std.VectorString

std::map<std::string, std::string>

std.MapStringString

命名风格#

Note

C# 绑定并未完全遵循 .NET 命名规范。以下描述的是当前生成代码的 实际形态, 调用时请以生成的 API 为准,不要按 C# 习惯自行改写名称。

  • 实例方法:多为 C++ 原样 snake_case,如 model.tool_list()region.name()param.to_json()

  • 静态工厂:多为 PascalCase,如 visionflow.img.Image.FromFile(...)visionflow.img.Image.Zeros(...)visionflow.Project.Create(...) / Open / Existsvisionflow.ProductInfo.Get()

  • 生命周期方法:原生包装对象通常提供 Dispose(),用于显式释放 native 资源。

  • 属性:命名不完全统一。VisionFlow 相关属性多保留 C++ 的 snake_case,如 sample_set_nameinput_tool_iddevice_name;.NET 集合和异常等框架侧 属性多为 PascalCase,如 ex.Type / ex.Code / ex.What / ex.Details、 容器的 .Count / .Keys / .IsEmpty

Warning

JSON 序列化为字符串是 to_json().to_string()不是 .NET 的 ToString()

节点名常量为 静态字段,标识符中的 . 被替换为 _,字符串值保留 .

string s1 = visionflow.Segmentation.pred;             // "pred"
string s2 = visionflow.Input.input_image_param;       // "input_image.param"
string s3 = visionflow.OCR.pred_characters;           // "pred.characters"

枚举值带 k 前缀:

var tag   = visionflow.SplitTag.kTrain;
var depth = visionflow.img.Image.Depth.kDepthU16;
var level = visionflow.compatible.Level.kFullyCompatible;

重载与默认参数#

C++ 的默认参数和重载通常会生成多个 C# 重载,而不是 C# 的可选参数。调用时应根据 参数个数选择对应的重载。

// C++ 原型(尾部两个参数带默认值):
//   void visionflow::helper::add_image_to_sample(
//       ISample& sample, const Image& image, const std::string& input_tool_id,
//       bool with_fingerprint = false, size_t thumbnail_long_side = 0);
// 这两个默认参数在 C# 里展开成三个重载,按参数个数选用:
visionflow_helper_global.add_image_to_sample(sample, image, input_id);              // with_fingerprint=false, thumbnail_long_side=0
visionflow_helper_global.add_image_to_sample(sample, image, input_id, true);        // with_fingerprint=true(导入时建议 true)
visionflow_helper_global.add_image_to_sample(sample, image, input_id, true, 512);   // 再加 thumbnail_long_side=512(缩略图长边)

类型转换:区分 CLR 继承转换与 dump/load 复制#

Note

CLR 是 Common Language Runtime (公共语言运行时)的缩写,即 .NET 执行 C# 程序的 运行环境。它负责托管对象的类型信息、继承关系、引用和垃圾回收。C# 的 as 是 CLR 提供的类型兼容转换运算符:当对象的实际托管类型是目标类型,或实现了目标接口时, as 返回一个指向同一对象的引用;类型不兼容时返回 null,不会抛出转换异常。

C++ 常用 property->as<visionflow::props::PolygonRegionList>() 这类成员模板做转换; C# 当前绑定 没有 等价的通用 as<T>()。C# 的 as 需要同时满足两个条件:

  1. 生成的 C# 类型之间存在 CLR 继承或接口关系;

  2. 返回值在托管侧实际是派生包装类,而不是仅仅持有派生 C++ 对象地址的基类包装类。

因此,不能仅凭两个类型存在 C++ 继承关系,或仅凭 native 指针实际指向派生对象, 就断定 C# as 一定可以成功。按以下两种情况处理:

情况 1:存在继承关系时,用 CLR 的 as(不经 dump/load)

// 假设 regions 是实际的 PolygonRegionList 包装对象,且 key 已存在。
// 当前生成代码中的 PolygonRegionList.at() 会构造 PolygonRegion 包装对象。
var regions = new visionflow.props.PolygonRegionList();
// ... 向 regions 添加 key 对应的区域(示例省略)...
visionflow.IRegion region = regions.at(key);
var polygon = region as visionflow.PolygonRegion;
if (polygon != null)
{
    // polygon 与 region 引用同一个托管包装对象
    polygon.set_ext_info("key", "value");
    Console.WriteLine(polygon.get_ext_info("key"));
}

// 显式强制转换在类型不兼容时会抛 InvalidCastException;不确定类型时优先使用 as:
// var polygon2 = (visionflow.PolygonRegion)region;

regions.at(key) as visionflow.PolygonRegion 能成功的具体原因是 SWIG 为 PolygonRegionList 生成了一个覆盖方法:

// 逻辑上等价于生成代码中的实现
public override visionflow.IRegion at(string key)
{
    return new visionflow.PolygonRegion(/* native 返回指针 */);
}

虽然方法声明的返回类型是 IRegion,但实际创建的托管对象是 visionflow.PolygonRegion,所以 CLR 的 as 可以成功。

当前 release 中,返回基类的 API 是否能使用 as,取决于生成代码实际创建的包装类。 可以按下面的规则判断,而不需要靠试错:

当前 release 中的典型返回包装#

API

生成代码实际创建的 C# 对象

能否直接向下 as

PolygonRegionList.at(key)

new visionflow.PolygonRegion(...)

可以转换为 visionflow.PolygonRegion

以基类 IRegionList 包装对象调用 at(key)

new visionflow.IRegion(...)

不能直接转换为具体区域类

sample.get(node_id)

new visionflow.props.IProperty(...)

不能直接转换为具体属性类

例如,sample.get 的返回类型是 IProperty,当前生成代码会构造 new IProperty(cPtr, true)

// sample.get 的返回类型是 IProperty;当前生成代码会构造 new IProperty(cPtr, true)
visionflow.props.IProperty property = sample.get(pred_node_id);
var polygon = property as visionflow.props.PolygonRegionList;
// polygon 可能为 null,即使 native 对象实际是 PolygonRegionList

// 如果 regions 的实际托管对象是 PolygonRegionList,调用的是它的 override,
// 因而会构造 PolygonRegion;如果实际托管对象只是 IRegionList,
// 则会构造 IRegion,不能向下转换。

这里失败的原因不是 C++ 没有继承关系。以当前生成代码为例,继承链实际是:

PolygonRegionList → IRegionList → IProperty

但如果 SWIG 返回时已经把指针包装成了 C# 的 IPropertyIRegion 实例, CLR 的 as 不会读取 native RTTI,也不会根据指针重新创建派生包装类。 此时应使用 dump/load 创建目标类型的包装对象。

情况 2:无法直接 CLR 转换、但类型提供 dump()/load() 时,用序列化复制

// 推理属性结果:sample.get(...) 返回 IProperty,用具体类型解码
var pred = new visionflow.props.PolygonRegionList();
using (var raw_property = sample.get(pred_node_id))
{
    if (raw_property == null)
        throw new InvalidOperationException("Property does not exist.");

    pred.load(raw_property.dump());
}

// 参数读写同理
var filter_args = new visionflow.param.PolygonsFilterParameters();
using (var raw_param = model.get_param(node))
{
    if (raw_param == null)
        throw new InvalidOperationException("Parameter does not exist.");

    filter_args.load(raw_param.dump());
}

这里的 load 不是把原对象“改成”另一个 C# 类型,而是把 dump 产生的序列化数据 读取到一个已经创建好的目标对象中。因此得到的是一个新的对象状态:

两种方式的区别#

方式

前提

是否序列化

对象身份和修改结果

CLR as

存在 CLR 继承/接口关系,且返回值实际是派生包装类

仍引用原对象;对对象的修改直接作用于原对象

dump/load

源类型和目标类型都支持兼容的 dump/load

创建序列化副本;修改副本不会自动回写原对象

Warning

dump/load序列化后的复制,不是 C++ as<T>() 那种引用转换:可能产生额外 拷贝,不能假定保留对象身份、共享状态或相同生命周期;load() 失败会抛出包装层异常, 处理方式见下文『异常模型』一节。没有 dump/load 的类型不能套用此法。

资源管理:RAII → IDisposable#

  • 需显式释放的原生包装类型通常实现 IDisposable;是否需要 Dispose 以生成的 C# 类型为准,不要假设所有返回对象释放语义相同。

  • GC 不保证及时、也不保证按依赖顺序 释放原生资源。

  • 为兼容 .NET Framework 4.8 示例工程,正文用传统 using (...) {}finally 中 显式 Dispose();调用方明确使用 C# 8+ 时才可改用 using var

依赖关系包括 Project SampleSet PropertySet PropertySetIteratorSampleSetIterator 和输入辅助对象依赖 SampleSet。 释放时应按逆依赖顺序进行:先释放辅助对象与迭代器,再释放 PropertySetSampleSet,最后释放 Project。 尤其应确保 SampleSet / PropertySet 在其所属 Project 之前 Dispose(),否则可能触发 native 侧的无效访问或其他不可恢复错误。

var prop_set = sample_set.property_set(node_id);
// ... 使用 prop_set ...
prop_set.Dispose();     // 先释放下游
sample_set.Dispose();   // 再释放 SampleSet
project.Dispose();      // 最后释放 Project

推荐让 using 变量按依赖关系从上游到下游依次声明,利用作用域结束时的逆序释放:

using (var project = visionflow.Project.Open(project_path))
using (var sample_set = project.main_sample_set())
using (var property_set = sample_set.property_set(node_id))
{
    // 使用 property_set
}

不要把子对象保存到比其 Project 更长的生命周期中;具体类型是否实现 IDisposable,仍以当前生成的 visionflow.cs 为准。

回调与 C# 继承#

部分原生虚函数通过 SWIG Director 映射为可被 C# 继承的包装类;当前常见类型有 visionflow.ILogSinkvisionflow.util.IProgressCallbackvisionflow.IScriptStdSink。它们在 C++ 中是接口类,但在生成的 C# API 中表现为可继承的 包装类。C# 子类用 override 重写虚方法即可被 C++ 回调;下面以前两个为例:

// 继承 ILogSink 并 override log,即可接收 C++ 侧的日志回调
public class MyLogSink : visionflow.ILogSink
{
    public override void log(int level, string message)
    {
        // 打上前缀,运行时即可看出日志确实回调进了这个自定义 sink
        Console.WriteLine($"[MyLogSink] level={level}: {message}");
    }
}

// 通过 InitOptions 注册;初始化后 C++ 产生的日志会回调进 C# 的 log()
var opts = new visionflow.InitOptions();
var log_sink = new MyLogSink();
opts.logger.custom_sink = log_sink;
visionflow_global.initialize(opts);

// custom_sink 是 native 侧保存的非拥有指针;只要日志系统仍会回调,
// log_sink 就必须由 C# 侧持有引用。确认日志系统不再使用后再 Dispose。

进度回调 IProgressCallback 同理——重写 on_progress,再注册到需要进度的对象上(如 ConfigureRuntimeProjectAdapter):

// 继承 IProgressCallback 并 override on_progress,接收进度回调
public class MyProgress : visionflow.util.IProgressCallback
{
    public override visionflow.TaskCtrlSignal on_progress(
        int total, int progress, string progress_info)
    {
        // 打上前缀,运行时即可看出进度确实回调进了这个自定义回调
        Console.WriteLine($"[MyProgress] {progress}/{total}: {progress_info}");
        return visionflow.TaskCtrlSignal.kContinue;   // 返回 kAbort 可请求中止任务
    }
}

// 注册到需要进度回调的对象上
var callback = new MyProgress();
adapter.set_progress_callback(callback);

// callback 必须保持有效到 adapter 生命周期结束;adapter 不会替调用方释放它。
// 释放顺序:先释放 adapter,再释放 callback。
// adapter.Dispose();
// callback.Dispose();

回调对象的有效期取决于 native 侧保存方式:IProgressCallback 至少要保持到持有它的 adapter 或 task 销毁;异步任务中应由外层对象保存引用,不能只依赖局部变量和 GC。 回调不再使用时,按生成类型的要求调用 Dispose(),并确保 native 侧已经不再回调。 回调方法中的异常也不应直接跨越 native 边界传播,应在回调内部捕获并转换为可由调用方 处理的状态。

值语义回写陷阱#

许多参数组对象的 get_xxx() 返回的是 副本:修改副本后必须用 set_xxx() 写回,否则改动不会生效。并非所有 getter 都有相同的返回语义,具体以生成的 C# API 和对应的 C++ 返回类型为准。

var infer_args = new visionflow.param.SegmentationInferenceParameters();
infer_args.load(model.get_param(node).dump());

var infer_shape = infer_args.get_infer_shape();  // 取回的是副本
infer_shape.set_enable(false);
infer_args.set_infer_shape(infer_shape);         // 必须写回,否则丢失

model.set_param(node, infer_args);

Note

具体的 get_/set_ 成员名以你所用发布包生成的 C# API 为准,可对照 visionflow/param/decl_params/segmentation_inference_parameters.py 核对。

异常模型#

派生自原生 visionflow::excepts::DefaultException 的异常,在 C# 中统一映射为托管类型 visionflow.excepts.DefaultException。用其字段区分,不能 catch VisionFlow 原生的具体子类 (如 FileNotFound)。

try
{
    var image = visionflow.img.Image.FromFile("not_exist_file.image");
}
catch (visionflow.excepts.DefaultException ex)
{
    // ex.Type == "class visionflow::excepts::FileNotFound"(原生类名字符串)
    // ex.Code == -10002
    Console.Error.WriteLine($"{ex.Type} ({ex.Code}): {ex.What}");
    Console.Error.WriteLine(ex.Details);
}

Warning

原生 std::exception 走另一条映射路径,转换为 System.SystemException, 因此不能把”所有原生异常”一概视为 DefaultException。应按异常的实际内容 (Type / Code)判断,而非依赖异常子类。

两条路径可以分别 catch。如果还要捕获 System.Exception,应将它放在最后; DefaultExceptionSystem.SystemException 彼此没有继承关系,二者的先后顺序 本身不影响匹配:

try
{
    // ... 调用 VisionFlow 接口 ...
}
catch (visionflow.excepts.DefaultException ex)
{
    // 源自 visionflow::excepts::DefaultException:有 Type / Code / What / Details
    Console.Error.WriteLine($"VisionFlow: {ex.Type} ({ex.Code}) {ex.What}");
}
catch (System.SystemException ex)
{
    // 源自原生 std::exception:只有 Message,没有 Type / Code
    Console.Error.WriteLine($"native std::exception: {ex.Message}");
}

容器语义#

std.VectorXxx、SWIG map、model.tool_list() 等返回的是 原生容器的 C# 包装, 不要将这些类型与 .NET 原生集合 List<T> / Dictionary<TKey, TValue> 混用。 它们实现了 .NET 集合接口,同时保留原生方法:

// 序列容器:.NET 风格成员 + 索引器 + 集合初始化器
var modules = new std.VectorString { "Segmentation", "Input" };
int n = modules.Count;
string first = modules[0];

// map:.Add / .ContainsKey / .TryGetValue / 索引器 / foreach(KeyValuePair)
var kv = new std.MapStringString();
kv.Add("k", "v");
if (kv.ContainsKey("k")) { }
string val;
kv.TryGetValue("k", out val);
foreach (var pair in kv) { var key = pair.Key; }

// 生成容器也可能有 .IsEmpty / .Contains 等
var tools = model.tool_list();
bool empty = tools.IsEmpty;
bool has = tools.Contains("Input");

Note

  • 不同容器暴露的成员不完全一致(.Count vs 原生 .size().Keys、索引器、 集合初始化器),以生成类型为准,不要假定都支持。

  • 是否需要 Dispose 以生成类型为准;若实现 IDisposable,须在其依赖对象仍有效时释放。

  • LINQ 仅对真正实现 IEnumerable<T> 的包装可用;能 foreach 不代表能直接 Select() / ToList()。需要在包装器释放后仍保留数据时,应先在其有效期内枚举 复制到托管集合。