mvdocs

# ImageDisplay 多格式图像集成

目标设计与 API 草案,尚未实施。总体边界和推进状态见设计概述。

本文负责异步捕获、稳定快照、多格式映射/取样、回执、预算和退出。API 为设计目标,不代表当前控件已支持彩色/深度。

关联:模型 · ToolFlow · 停止 · 示例

# ImageDisplay 直接消费 IPixelImage

ImageDisplay 是图像的只读消费者,与算法节点使用同一个 IPixelImage 模型。控件直接接收 IGray8Image、IRgb24Image、IBgr24Image、IDepth16Image 或其公共接口,不要求调用方创建第二种显示来源,不再设计 PixelImageSource。

相机 / 文件解码 / 算法输出
             ↓
         IPixelImage
          ↙       ↘
算法自行打开读写     ImageDisplay 自行打开读取
                          ↓
                   内部不可变显示快照
                          ↓
                 格式转换 / mip / tile
                          ↓
                  UI 取样与 Skia 绘制

IPixelImage 可以是稳定只读图像,也可以是可再次写入的根图像。控件统一从中取得并保留稳定读取,形成独立显示快照。原图写入不会自动刷新屏幕;宿主在写入完成后再次 SetImage。同一实例重复设置合法,不依赖图像编号递增,不订阅每次缓冲写入来隐式推进显示。

# 文件与 SDK 输入在进入控件前形成图像

文件、资源、网络与编码字节先通过图像工厂解码为 IPixelImage,再提交控件。已有 ImageSource 及应用 loader 留在图像创建层,ImageDisplay.Source 改为 IPixelImage?,不同时接受编码来源与像素图像,也不新增 object 类型入口。

Copy 位于无 UI 的 PixelImage 工厂;编码读取放在现有控件/图像加载集成层的 PixelImageDecoder,不让基础包依赖 ImageSource:

// 现有 ImageDisplay 包的 Avanti.Toolkit.MachineVision 命名空间。
public static class PixelImageDecoder
{
    public static ValueTask<IPixelImage> LoadAsync(
        ImageSource source, CancellationToken cancellationToken = default);
}

Copy 同步复制逻辑像素,默认返回稳定 ReadOnly 图像,适合采集发布。显式 access=ReadWrite 只用于自有输出构建或流程外编辑,不能直接当作已发布的工具流下游输入提交(须先稳定发布)。允许整理排列,保留格式、编码与业务信息。SDK 临时指针同步 Copy 后不交给异步显示工作。

LoadAsync 是拟议解码适配成员,复用已有读取服务,返回稳定 ReadOnly 图像,不是当前已有 API。Gray8 保持灰度,支持的无透明彩色栅格输出 Rgb24/Bgr24 并声明其实际编码;不为显示一律转换为 Gray8。alpha、色彩管理、方向信息按 decoder 契约处理,未支持的输入明确拒绝,不静默丢弃透明度。非 UI 宿主可接入自己的 codec 并通过 PixelImage 工厂创建同样的图像,不依赖此适配器。

文件加载顺序由宿主管理。若 A 加载慢、B 后加载却先完成,不能直接在 A 完成时覆盖 B;绑定宿主应在加载提交边界校验自己的请求代次或取消旧加载。控件只能对已经登记的显示请求排序,不能推断外部尚未完成的加载意图。

# 多窗口与所有权

同一个有效 IPixelImage 可重复提交或供多个控件使用。每个控件先取得自己的 Share,再建立显示读取;不会接管或 Dispose 调用方传入的 handle。对 ReadOnly 输入,Share 已延长稳定内容寿命,后续 OpenRead 复用已有授权;对 ReadWrite 输入,登记只保留存储,实际授予读取时才确定版本。

Source 属性保留的是调用方对象引用,不代表资源所有权转移。命令式 SetImage 成功登记后,调用方可关闭自己的 handle,本次更新仍由控件的独立引用支撑;重挂载、再次属性更新或提交需要有效输入。绑定宿主保持当前图像有效,并在替换/关闭时释放自己旧的 handle;异步属性通知不得在控件取得 Share 之前就关闭对应输入。实时预览优先使用有所有权的槽位和 UI 同步登记确认,避免绑定对象把 SDK 原图长期保留。控件不能关闭借入 Source 替别人释放资源。

# ImageDisplay 的设置、捕获与显示状态

public enum ImageInputRetention
{
    RetainInput = 0,
    Snapshot = 1
}

public enum DepthColorMap
{
    Grayscale = 0,
    Viridis = 1
}

public sealed record DepthDisplayOptions
{
    public DepthDisplayOptions(
        ushort minimumRawValue = 0,
        ushort maximumRawValue = ushort.MaxValue,
        DepthColorMap colorMap = DepthColorMap.Grayscale);

    public ushort MinimumRawValue { get; }
    public ushort MaximumRawValue { get; }
    public DepthColorMap ColorMap { get; }
    public Color InvalidColor { get; init; } // 默认透明,来自 Avalonia.Media。
}

public sealed class ImageDisplay
{
    public static readonly StyledProperty<IPixelImage?> SourceProperty;
    public IPixelImage? Source { get; set; }

    public static readonly StyledProperty<ImageInputRetention> InputRetentionProperty;
    public ImageInputRetention InputRetention { get; set; }

    public static readonly StyledProperty<DepthDisplayOptions> DepthDisplayProperty;
    public DepthDisplayOptions DepthDisplay { get; set; }

    public ImageDisplayUpdate? LastUpdate { get; }
    public ImageInfo? CurrentImageInfo { get; }
    public ImageContentVersion? CurrentVersion { get; }
    public Guid? CurrentUpdateId { get; }

    public ImageDisplayUpdate SetImage(
        IPixelImage? image,
        ImageInputRetention? inputRetention = null,
        ImageContentVersion? expectedVersion = null);

    public ValueTask ClearImageAsync(CancellationToken cancellationToken = default);
    public bool TryGetPixel(Point viewPosition, out ImagePixel pixel);
    public event EventHandler<ImageRenderedEventArgs>? ImageRendered;
}

Source、InputRetention 与 DepthDisplay 是真正的 Avalonia styled property,适合 AXAML/绑定;非法枚举、空 DepthDisplay、非法深度范围均拒绝。Current 系列和 LastUpdate 是只读查询,不返回带权限的图像对象。

# 请求登记与读取授予是不同时间点

Source 变化和 SetImage 共用一个更新管线。SetImage 在 UI 线程调用,通过 SetCurrentValue 更新 Source,保留绑定;显式调用即使传入相同实例也登记新请求,不能因为属性相等而漏掉更新。属性变化与显式命令必须只产生一个请求,不保留两套各自驱动画面的状态。

同步登记时验证线程、输入 Info、接口/格式一致性与 expectedVersion 的 BufferId,取得 image.Share() 作为控件自己的输入引用,并预留可确定的输入保留预算。参数错误、已关闭输入或无法取得 Share 时,在修改显示意图前抛错;资源预算不足则登记 Failed 回执,释放这次取得的引用并保留旧画面。绑定回调中的错误形成 Failed 回执并通过既有 Error 报告。空图像不得同时提供 expectedVersion。

SetImage 返回不承诺已经建立显示读取;稳定输入的 Share 已固定内容,可写输入则在后台授予时捕获。后台准备调用输入引用的 OpenReadAsync 或带 expectedVersion 的重载,不在 UI 等待写者。ReadOnly 输入复用稳定授权;ReadWrite 输入若正在写入,控件异步等待。替换或卸载时取消申请,授予/取消竞争保证所得对象恰好释放一次。

不提供 expectedVersion 时显示授予读取时的当前内容;提供时必须得到指定版本,否则以 VersionMismatch 失败,保留原画面。提交参数中的 ImageId/CapturedAt 不参与判断。需要提交算法刚完成的确切内容时,算法保存 Complete 返回的版本,结束写入后 SetImage(image, expectedVersion: version)。

同一个 IPixelImage 在之后被改写,不改变当前快照。Source 对相同实例赋值可能不触发属性变化;宿主应显式 SetImage 申请新读取。若输入已经处于 RetainInput 显示状态,原地写入会等待现有快照退出,不能期待“先写入,再 SetImage”自动打破这个等待。

若控件当前仍持有本次输入同一实际 BufferId 的稳定原图读取,新请求优先从该已授予读取 Share,再按新输入 Info 校验和绑定视图,而不重新排队 OpenReadAsync。这样可避免“旧显示持有读 → 写者排队 → 新显示读排在写者后 → 等新显示安装才关闭旧读”的自锁。新输入只是同一分配的另一个 ROI 时也采用此规则;指定 expectedVersion 与现有稳定版本不同时立即报告 VersionMismatch。此处判断的是实际读取的 BufferId,不能拿已脱离副本的公开来源 Version 来冒充仍持有原图读取。

# 内部显示快照及唯一状态所有者

内部 DisplaySnapshot 是控件私有的不可变内容持有者,不是新增公共图像模型。它包含本次捕获的输入 Info/Version、实际读取对象、私有存储的排列、准备好的派生层及其资源账本。UI 当前内容、后台 mip、渲染状态与在途消息各自持有内部快照引用;最后一个引用退出才关闭对应读取或私有像素。

状态 唯一所有者与作用
期望输入 UI 的 Source 与最新请求代次,表示宿主最后登记的意图
待处理候选 输入 Share、策略和可选期望版本;只保留最新候选
准备中的读取 一个受控后台准备工作,负责权限申请、可选复制与格式准备
当前显示内容 UI 当前 DisplaySnapshot;新候选失败时保持它
实际渲染状态 render handler 独立持有快照,禁止直接读取 Source
更新回执 只保存结果和释放信号,不反向保留快照或控件

新请求取消旧候选/准备,完成时校验 UI 请求代次,过时工作不得覆盖新意图。已经进入渲染的旧快照等待其实际退出;取消令牌、消息合并或 UI 换图不代表渲染已停止读取。

每个控件最多一个受控准备工作和一个最新候选;新准备等待旧工作安全退出。准备任务不能同步等待 UI,避免换图和卸载死锁。渲染待发状态只保留最新快照,不能排队积累带原图引用的每次 SetImage。派生层工作受并发上限与预算控制,取消后仍在运行的工作继续计量。

当前图像在候选安装成功时原子切换:UI 查询、Info/Version、像素取样与渲染消息都引用同一份内容。新候选失败保留旧内容并报告 Error,不能让新 Source 的信息和旧画面混合。

隐藏、零尺寸或未挂载不阻止合法请求的 CPU 准备与安装,不保证实际绘制。卸载取消候选、清除 UI 快照并通知 render handler 退出;安全排空继续异步完成,不在 UI 同步等待。之后重挂载把仍有效 Source 作为新请求,不复用已经失效的读取对象。

# 输入保留与显示副本

策略 像素访问 对后续算法写入的影响
RetainInput,默认 显示快照持有原图的稳定读取 原地写入等待控件和其他读取者退出
Snapshot 在稳定读取期间复制完整逻辑原图,再关闭原图读取与输入引用 控件完成 InputReleased 后不再阻止原图写入
DetachInputAsync 将当前保留的原图转成完整显示副本 按需保存当前显示,释放这次更新对原图的访问

InputRetention 的属性值只影响未来请求,SetImage 参数只覆盖本次请求。不会因为样式改动偷偷改变当前图像的存储。对当前共享内容主动脱离使用对应 update.DetachInputAsync。

显示副本保留原始格式及所有逻辑像素,包括深度原始值,不是只保留可见 tile、8 位深度预览或已经调过曝光的位图。这样才能继续像素查询、改变深度范围、缩放和平移,并在图形上下文重建时恢复。

快照复制可以整理实际存储排列。CurrentImageInfo 和 CurrentVersion 始终描述本次捕获的输入信息及来源版本;副本内部使用自己的存储信息与存储版本。脱离不改写原始内容身份,不产生新的业务图像提交。UI/渲染缓存不能拿公开的来源版本冒充私有存储版本。

InputReleased 只确认控件此次更新的输入生命周期引用和原图读取均已结束。Source 所引用的调用方对象、其他算法访问或另一个控件不在其中;Source 属性本身没有因此被置空。Snapshot 后即使屏幕继续显示旧图,原图也可被写入;需要刷新时再次 SetImage。

# 多格式渲染与显示参数

本目标设计完整覆盖 Gray8、Rgb24、Bgr24 与 Depth16,不再把模型支持的三类扩展图像一律作为 UnsupportedImage。实施可以分阶段交付,但完成本设计需要分别验证四种格式。

输入 显示处理 原始像素查询
Gray8 现有灰度曝光/对比度等映射 原 byte 灰度
Rgb24 按 R/G/B 解释,上传为渲染后端需要的颜色格式 原 Rgb24
Bgr24 按 B/G/R 解释;仅在上传或派生存储时重排 原 Bgr24,不伪装为 RGB 内存
Depth16 按原始值范围归一化,应用灰度或 Viridis 色表 原 ushort、有效性及可用时的米制深度

DepthDisplayOptions 要求 MinimumRawValue < MaximumRawValue。归一化为 clamp((raw - min) / (max - min), 0, 1),差值提升为有符号或浮点计算,避免 ushort 下溢。先按 Info.InvalidDepthValue 排除无效码,使用 InvalidColor;未声明无效码时零也是普通值。默认范围为完整 ushort 区间,不按当前图像自动拉伸,不隐含逐帧扫描。需要自动范围时由宿主统计后设置。

深度显示范围明确使用原始值;Info.DepthUnitMeters 只用于真实深度查询及宿主单位换算。尺度未声明时仍可显示原始深度,但不能报告米制值。改变深度显示范围或色表只更新渲染配置,不写入原图,不重新申请原图读取,也不更改 CurrentVersion。

既有灰度参数只作用于 Gray8;RGB/BGR 按 Info.ColorEncoding 处理,LinearRgb 在显示端编码为显示值,Srgb 按 sRGB 显示。Unspecified 首版按 8 位显示编码解释并产生可观察的编码假定诊断,不宣称进行了色彩标定。灰度阈值不应用到彩色单通道。Depth16 使用自己的映射;未支持的 HDR、alpha、平面多通道或其他编码明确拒绝。

Gray8 使用现有灰度 mip;RGB/BGR 按通道处理并明确线性化/再编码策略,不混用通道顺序或色彩编码。Depth16 不对含无效码的原始 ushort 简单平均;首版在当前映射后生成颜色派生层,改变范围/色表使其失效。原始值保持不变,TryGetPixel 永远查询原图,不返回预览平均值。

缓存分两层:原始内容键由实际存储版本、格式、尺寸、步长与偏移确定;映射后的 mip/tile 还包含 ColorEncoding、深度无效码、显示参数与 GPU context 身份。相同字节换一种编码或无效值解释,也必须重新映射。参数变化创建新的渲染配置代次,旧工作不能安装过时颜色。业务编号、时间与 ROI 原点不替代像素内容键,内存保留与显示映射分别计费。

# 多格式像素查询与坐标

public readonly struct ImagePixel
{
    public Guid UpdateId { get; }
    public ImageContentVersion Version { get; }
    public ImagePixelFormat PixelFormat { get; }
    public int X { get; }
    public int Y { get; }
    public bool IsValid { get; }

    public bool TryGetGray8(out byte value);
    public bool TryGetRgb24(out Rgb24 value);
    public bool TryGetBgr24(out Bgr24 value);
    public bool TryGetDepth16(out ushort rawValue);
    public bool TryGetDepthMeters(out double meters);
}

ImagePixel 是不持有像素资源的值快照,内部按 PixelFormat 保存恰当的原始值。TryGetGray8/Rgb24/Bgr24/Depth16 仅在格式相符时成功,不隐式转换。Depth16 的 rawValue 即使是无效码仍可读取,IsValid 为 false;TryGetDepthMeters 只有尺度已声明且该原始值有效时成功。当前四种格式的 Gray/RGB/BGR 样本 IsValid 为 true。

TryGetPixel 必须在 UI 线程调用,按控件现有逆变换将控件坐标映射到当前图像像素,检查边界后取逻辑像素坐标。X/Y 是 ROI 内坐标;采集坐标可加 CurrentImageInfo.OriginX/OriginY。负步长和 ROI 偏移由临时读取封装处理,不能用 viewPosition 直接作缓冲偏移。

无当前图像或坐标在图像外返回 false;深度无效码仍返回 true 的样本,由 IsValid 表达。结果关联当前 UpdateId/Version,不包含颜色映射后的屏幕颜色。像素工具继续调用该 API,根据 PixelFormat 展示灰度、颜色通道或深度单位,不访问 Source,也不依赖临时读取对象逃逸。

# 回执、绘制和输入释放

确认 含义
SetImage 返回 已登记并取得独立图像 Share;稳定输入保持固定内容,可写输入尚未必授予读取
Completion Installed、Cleared、Superseded、Unloaded 或 Failed
InputReleased 此次更新全部原图引用与读取已退出,包括未授予时的候选引用
ImageRendered 某份已安装内容发生绘制;可重复,也可能从未发生
buffer.ReturnCompleted 全部参与者退出后,底层 owner/SDK 真实归还完成
public enum ImageUpdateStatus
{
    Installed = 0,
    Cleared = 1,
    Superseded = 2,
    Unloaded = 3,
    Failed = 4
}

public enum ImageUpdateFailure
{
    None = 0,
    CpuBudgetExceeded = 1,
    UnsupportedImage = 2,
    ReadFailed = 3,
    PreparationFailed = 4,
    VersionMismatch = 5
}

public sealed class ImageUpdateResult
{
    public ImageUpdateStatus Status { get; }
    public ImageInfo? Info { get; }
    public ImageContentVersion? Version { get; }
    public ImageUpdateFailure Failure { get; }
    public Exception? Error { get; }
}

public enum ImageInputDetachResult
{
    Released = 0,
    CpuBudgetExceeded = 1
}

public sealed class ImageDisplayUpdate
{
    public Guid UpdateId { get; }
    public Task<ImageUpdateResult> Completion { get; }
    public Task InputReleased { get; }
    public ValueTask<ImageInputDetachResult> DetachInputAsync(
        CancellationToken cancellationToken = default);
}

public sealed class ImageRenderedEventArgs : EventArgs
{
    public Guid UpdateId { get; }
    public ImageInfo Info { get; }
    public ImageContentVersion Version { get; }
    public TimeSpan RenderDuration { get; }
    public DateTimeOffset RenderedAt { get; }
}

UpdateId 是无业务排序意义的 Guid,不是帧号。Installed 时 Info/Version 非空,Failure 为 None、Error 为 null;失败时带类别与 Error,Info/Version 为空;其他终态不报告图像信息或异常。Installed 不等于 Rendered,更不等于物理屏幕扫描输出。已安装结果不因后来换图改写为 Superseded。

未安装的过时请求可以立即完成 Superseded,但 InputReleased 必须等待实际资源退出。卸载使未完成安装的请求得到 Unloaded。失败路径也必须释放候选 Share、已经取得的读取、复制临时存储与预算。回执不实现 IDisposable,不持有快照或控件强引用;活跃更新的脱离操作通过弱关联委托给控件状态。内部资源工作按需保留自身引用直到安全退出,然后解除关联;保存历史回执不能间接保留窗口或输入。

Completion 与 InputReleased 的先后没有统一保证:Snapshot 可以先结束原图访问再完成安装,RetainInput 则通常在安装后继续持有原图。空图像请求自身的 InputReleased 立即完成;ClearImageAsync 另行等待此前更新,不把这两个信号混为一谈。

ImageRendered 通知 UI 调度时携带发生绘制时捕获的内容身份,不把后来 CurrentVersion 填入较早事件。平移、缩放和显示参数变化可重复绘制;无逐张通知承诺。绘制失败通过既有 State/Error 报告,不改写已完成的 Installed。

# 脱离和清空

DetachInputAsync 可从任意线程调用,由内部调度到 UI 协调更新状态,复制仍在后台完成。它处理一个更新的整个输入寿命:尚未取得读取时,把该请求转为必须保存副本;正在复制时复用工作;已安装 RetainInput 时从稳定读取复制全图并安全切换;已经失败、替换或卸载则等待该更新的原图访问排空。不得因为“还没有读取对象”就提前宣称输入已释放,候选 Share 也保留资源。

成功 Released 时 InputReleased 已完成。复制预算不足时返回 CpuBudgetExceeded,不破坏当前内容及其原图读取;实际复制异常向等待方报告,原显示保持。后续可以重试。多次并发脱离复用同一份工作;成功后幂等。若在途安装或替换竞争,依据 UpdateId 与内部代次完成退出,不复活旧图像。

复制切换只更换内部存储,公开 CurrentImageInfo/CurrentVersion 保持来源内容身份;UI 取样、mip、消息和渲染全部退出旧存储读取后,才完成 InputReleased。开始前取消可以阻止本次脱离;开始后取消仅取消等待,安全退出继续。

SetImage(null)/Source = null 清除当前显示意图并登记 Cleared 请求,不同步等待旧渲染。ClearImageAsync 在 UI 发起同样清空,另行捕获此前相关更新并等待其输入全部退出;随后提交的新图像不属于该屏障,不能被旧清空任务撤销。操作开始后取消仅取消调用方等待。该屏障不等待调用方自己的图像、算法访问或 SDK 全局归还。

# 预算、线程与资源归还

CPU 账本包含控件保留原图的 RetainedByteLength、私有完整副本、mip、颜色映射暂存和上传准备。IPixelImage 增加只读 RetainedByteLength,取得输入 Share 后即可保守预留;同一 BufferId 在同一控件中去重计费,重叠 ROI 不重复当成独立分配。不同控件读数可能重叠,不能相加作为物理进程总量。若底层较大分配只暴露局部视图,由 SDK 适配器报告实际保留成本。

CPU/GPU 预算沿用现有控件实例账本及配置时机。GPU 按实际上传格式及纹理占用保守计费,不能用 Gray8 的 1 字节成本估算 RGB/depth 派生纹理。不宣称约束图形驱动或 Avalonia 全局缓存。缩放层、退役工作和 context 重建各自有释放归属。

复制峰值必须容纳当前画面、候选原图、私有副本和仍运行的旧派生任务。不能先丢旧图像凑预算,再声称失败保留旧画面。预算失败不建立自动重试队列,交给宿主决定何时再提交。

Source/SetImage、显示参数、当前属性及 TryGetPixel 在 UI 线程处理;权限等待、复制和 CPU 派生在受控后台执行;render handler 只消费不可变快照。不在 UI Wait/Result,也不持有后台需要 UI 释放的锁。模板和主题不承担图像生命周期,替换模板不新增第二套来源管线;现有坐标、焦点、裁剪和覆盖层职责继续保留。

权限申请直接传取消令牌,不能外套可能遗失资源返回值的 WaitAsync。对 Completion/InputReleased/ReturnCompleted 可以用 WaitAsync 取消自己的等待,不改变底层事实。图像 Share、临时读取、native pin 和 SDK 引用都必须显式关闭,不能依赖 GC 完成归还。

不得持有自己的读取等待原地写入;不得在持有最后一个图像/buffer handle 时等待 ReturnCompleted。InputReleased 不关闭 Source 指向的调用方 handle。需要 SDK 真正归还时,先记录 ReturnCompleted,关闭自己的 handle 和访问,再等待;SDK release 回调仍由存储生命周期定义的归还调度执行一次。