# 机器视觉图像与显示使用示例
目标设计与 API 草案,尚未实施。总体边界和推进状态见设计概述。
本文集中展示契约,不构成节点框架或现有可执行代码。所有权、接管和线程调度须与各专题共同阅读。
# 节点示例:自行读取输入、写入输出
// 只展示处理一个图像项的辅助方法,不是整个流程节点的执行模型。
public sealed class ThresholdOperation
{
public async ValueTask ProcessOneAsync(
IGray8Image input,
IImageOutputCollector outputs,
CancellationToken cancellationToken)
{
using var source = await input.OpenReadAsync(cancellationToken);
using var output = PixelImage.RentGray8(new ImageInfo(source.Width, source.Height)
{
ImageId = source.Info.ImageId,
CapturedAt = source.Info.CapturedAt,
OriginX = source.Info.OriginX,
OriginY = source.Info.OriginY
});
using var target = await output.OpenWriteAsync(cancellationToken);
ProcessRows(source, target, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
using var published = target.CompleteAndOpenRead();
var result = (IGray8Image)published.ShareImage();
try
{
outputs.AddOwned("Images", result);
}
catch
{
result.Dispose();
throw;
}
}
private static void ProcessRows(
ReadOnlyImage<byte> source,
WritableImage<byte> target,
CancellationToken cancellationToken)
{
for (var y = 0; y < source.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
var sourceRow = source.GetPixelRow(y);
var targetRow = target.GetPixelRow(y);
for (var x = 0; x < source.Width; x++)
{
targetRow[x] = sourceRow[x] >= 128 ? (byte)255 : (byte)0;
}
}
}
}
一个实际节点可以多次调用上述辅助方法、结合多个输入、输出零项或多项。每个输入借用和每个输出接管都独立计数。ShareImage 实际对象保留 Gray8 接口,此处转换只是受验证的接口断言。节点自行完成写入并释放临时访问,执行上下文只接管已经准备好的输出。
# 彩色与深度节点的扩展方式
| 节点 | 输入与输出契约 | 节点内部读取 |
|---|---|---|
| RGB 转灰度 | IRgb24Image 输入项、IGray8Image 输出项 | ReadOnlyImage<Rgb24>,逐像素使用 R/G/B |
| BGR 转 RGB | IBgr24Image 输入项、IRgb24Image 输出项 | ReadOnlyImage<Bgr24>,明确重排通道 |
| 深度范围筛选 | IDepth16Image 输入项、IGray8Image 输出项 | ReadOnlyImage<ushort>,先排除无效码再按尺度判断 |
| 深度滤波 | IDepth16Image 输入项及输出项 | 保留尺度与无效码,节点决定是否写入副本 |
| 通用图像保存 | IPixelImage 输入项,可没有图像输出 | 非泛型 GetPixelBytes 结合 Info.PixelFormat 处理 |
例如深度节点自行打开其输入:
static async ValueTask<double?> ReadDepthMetersAsync(
IDepth16Image input, int x, int y, CancellationToken token)
{
using var image = await input.OpenReadAsync(token);
return ReadDepthMeters(image, x, y);
}
static double? ReadDepthMeters(ReadOnlyImage<ushort> image, int x, int y)
{
var raw = image.GetPixelRow(y)[x];
if (image.Info.InvalidDepthValue is ushort invalid && raw == invalid)
{
return null;
}
if (image.Info.DepthUnitMeters is not double unit)
{
throw new InvalidOperationException("Depth scale is required.");
}
return raw * unit;
}
像素格式决定类型化访问方式,不决定节点访问期限或读写权限。新增格式时同步扩展格式枚举、信息验证、类型化工厂与临时访问桥接;确有业务语义差异时增加命名接口,不为每种算法另造图像接口。
# 使用示例
以下是目标接口示例,尚不能用于当前源码。控件调用在 UI 线程执行;图像工厂、文件解码和算法按宿主调度执行,不依靠示例中的上下文捕获推断真实线程。
# 算法图像直接显示
// image 是流程输出项,例如 IGray8Image / IRgb24Image / IDepth16Image。
var update = viewer.SetImage(image);
var result = await update.Completion.WaitAsync(token);
if (result.Status == ImageUpdateStatus.Failed)
{
// 由宿主呈现 result.Error 或选择下一次提交。
}
此调用不转移 image 的所有权。算法若只读取可并发申请读取;若需要写入同一分配,先选择 Snapshot、脱离或自行克隆。
viewer.SetImage(image);
viewer.SetImage(image); // 显式再次捕获,无编号递增要求。
# 保留屏幕旧图,原图继续修改
static async Task<ImageDisplayUpdate> ModifyAndShowAsync(
ImageDisplay viewer,
IGray8Image image,
CancellationToken token)
{
if (image.Access != ImageBufferAccess.ReadWrite)
{
throw new ArgumentException("本例要求可独占编辑的图像;稳定输入应先复制。", nameof(image));
}
var first = viewer.SetImage(image, ImageInputRetention.Snapshot);
var installed = await first.Completion.WaitAsync(token);
if (installed.Status != ImageUpdateStatus.Installed)
{
throw new InvalidOperationException("显示输入未成功安装。");
}
await first.InputReleased.WaitAsync(token);
ImageContentVersion version;
using (var writable = await image.OpenWriteAsync(installed.Version!.Value, token))
{
// 示例同步处理;耗时算法应在后台调度后再回 UI 提交。
InvertGray(writable);
token.ThrowIfCancellationRequested();
version = writable.Complete();
}
return viewer.SetImage(image, expectedVersion: version);
}
static void InvertGray(WritableImage<byte> image)
{
for (var y = 0; y < image.Height; y++)
{
var row = image.GetPixelRow(y);
for (var x = 0; x < row.Length; x++)
{
row[x] = (byte)(255 - row[x]);
}
}
}
本例处理的是明确独占使用的可写图像,不是普通任务边上的稳定只读输入。Snapshot 不会把 ReadOnly handle 升级为可写。它只结束 first 的输入访问;其他稳定图像、窗口或读取尚在时仍会等待,其他写者抢先修改则期望版本失败。RetainInput 可用 DetachInputAsync 保存旧画面并释放本次访问。
# 文件与 SDK 临时图像
using var fileImage = await PixelImageDecoder.LoadAsync(
new FileImageSource(@"D:\Images\sample.png"), token);
var update = viewer.SetImage(fileImage);
本次 SetImage 已有自己的 Share,调用方之后可以关闭 fileImage;绑定重用和重新挂载需要宿主继续保持原对象有效。并发文件加载的意图顺序由宿主在调用 SetImage 前验证。
static unsafe IPixelImage CopyCameraCallbackImage(
nint address, int accessibleLength, ImageInfo info)
{
var pixels = new ReadOnlySpan<byte>((void*)address, accessibleLength);
return PixelImage.Copy(pixels, info);
}
SDK 保证同步复制期间可读,返回后不访问临时指针。返回的 IPixelImage 由调用方持有并调度 UI 提交;在 UI 调度前累积的图像也需要宿主限流和释放。长期 SDK 引用则通过 ImageBuffer.ImportNative 和 PixelImage.Create 包装,不必为显示另造来源对象。
# 彩色与深度取样
viewer.DepthDisplay = new DepthDisplayOptions(
minimumRawValue: 500,
maximumRawValue: 5000,
colorMap: DepthColorMap.Viridis);
viewer.SetImage(depthImage);
if (viewer.TryGetPixel(pointerPosition, out var pixel))
{
if (pixel.TryGetDepthMeters(out var meters))
{
ShowDepth(meters);
}
else if (pixel.TryGetDepth16(out var raw))
{
ShowRawDepth(raw, pixel.IsValid);
}
else if (pixel.TryGetRgb24(out var rgb))
{
ShowColor(rgb.R, rgb.G, rgb.B);
}
}
工具使用样本自身的格式与身份,不把用户刚设置的新 Source 当作当前已显示图像。BGR 工具分支相应调用 TryGetBgr24;深度无效值不能用普通物理距离显示。