NjStdVision.Controls 1.0.2

dotnet add package NjStdVision.Controls --version 1.0.2
                    
NuGet\Install-Package NjStdVision.Controls -Version 1.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NjStdVision.Controls" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NjStdVision.Controls" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="NjStdVision.Controls" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NjStdVision.Controls --version 1.0.2
                    
#r "nuget: NjStdVision.Controls, 1.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NjStdVision.Controls@1.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NjStdVision.Controls&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=NjStdVision.Controls&version=1.0.2
                    
Install as a Cake Tool

NjStdVision.Controls 详细使用文档

基于 Avalonia 11 + SkiaSharp 构建的工业视觉图像显示控件库
Target Framework: .NET 8.0 | 支持静态 SKImage 与实时 ImagePixelBuffer 双显示路径


目录

  1. 快速开始
  2. ImageDisplayControl — 核心控件
  3. Behaviors — 行为扩展
  4. ROI 类型详解
  5. 数据模型
  6. 转换器 (Converters)
  7. 接口 (Interfaces)
  8. 典型使用场景示例
  9. 架构与扩展
  10. 常见问题

1. 快速开始

安装 / 引用

方式 A:项目引用(开发期)

<ItemGroup>
  <ProjectReference Include="..\NjStdVision.Controls\NjStdVision.Controls.csproj" />
</ItemGroup>

方式 B:NuGet 包(推荐给其他项目)

dotnet add package NjStdVision.Controls --version 1.0.2

或手动添加:

<ItemGroup>
  <PackageReference Include="NjStdVision.Controls" Version="1.0.2" />
</ItemGroup>

消费方 Avalonia / Xaml.Behaviors 版本须与库一致(当前 11.3.8),否则 Behavior 可能不兼容。

库已声明的 NuGet 依赖(无需在消费方重复添加):

包名 版本
Avalonia 11.3.8
Avalonia.Skia 11.3.8
Xaml.Behaviors 11.3.8
System.Reactive 6.1.0

SkiaSharp 由 Avalonia 间接引入,无需单独添加。

XAML 命名空间声明

在每个使用该库的 .axaml 文件顶部加入:

xmlns:controls="clr-namespace:NjStdVision.Controls;assembly=NjStdVision.Controls"
xmlns:behaviors="clr-namespace:NjStdVision.Controls.Behaviors;assembly=NjStdVision.Controls"
xmlns:rois="clr-namespace:NjStdVision.Controls.Rois;assembly=NjStdVision.Controls"
xmlns:converters="clr-namespace:NjStdVision.Controls.Converters;assembly=NjStdVision.Controls"
xmlns:ix="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"

2. ImageDisplayControl — 核心控件

ImageDisplayControl 是整个库的核心,继承自 Avalonia.Controls.Control,专为显示 SkiaSharp SKImage 而设计,支持:

  • 通过变换矩阵 (Matrix) 实现平移和缩放
  • 自适应窗口大小(IsFitSize
  • 通过附加 Behavior 扩展所有交互能力

属性一览

属性 类型 默认值 说明
SkiaImage SKImage? null 静态图像显示源(文件加载等)
PixelBuffer ImagePixelBuffer? null 实时采图首选;设置后优先于 SkiaImage 用于绘制
HasImage bool 只读;PixelBuffer 已分配或 SkiaImage 非空
TransformValue Matrix Matrix.Identity 当前视图变换矩阵(含平移和缩放)
IsFitSize bool false 设为 true 时自动将图像缩放适配到控件大小
InteractionMode ImageInteractionMode Navigate 协调多 Behavior 输入优先级(见下文)
ViewportStatus ImageViewportStatus Empty 视口状态(坐标、缩放、像素),只读输出
ZoomScale double 当前缩放比例(TransformValue.M11
ImageBounds Rect? 图像像素边界 (0,0,W,H),自动识别 PixelBuffer / SkiaImage

方法

方法 说明
FitSize() 计算并应用适配变换,使图像居中显示且不超出控件边界
TryGetImageSize(out width, out height) 获取当前图像尺寸(兼容 PixelBufferSkiaImage
ScreenToImagePoint(Point) 屏幕坐标 → 图像坐标
ImageToScreenPoint(Point) 图像坐标 → 屏幕坐标
ImageRegionToScreenRect(Rect) 图像区域 → 屏幕包围矩形
GetVisibleImageRect() 当前视口在图像坐标系中的可见区域(用于视口裁剪)
InvalidateOverlay() 请求重绘标注层(ROI、自定义叠加)
InvalidateImageLayer() 请求重绘底图层(图像或视口变换变化)
NotifyPixelBufferUpdated() 外部已写入 PixelBuffer 像素后请求底图重绘(等价于 InvalidateImageLayer
InvalidateImageRegion(Rect, padding) 按图像坐标脏区请求标注层重绘(同帧合并)
UpdateViewportStatus(Point, PixelInfo?) 更新视口状态栏数据
ClearViewportStatus() 清空视口状态(鼠标移出时)

ImageInteractionMode

通过 InteractionMode 属性协调 ImageDragBehaviorRoiCreationBehaviorRoiInteractionBehavior 等行为的输入互斥:

说明
Navigate 平移 + 缩放(左键拖动画布)
RoiCreate 创建 ROI(左键拖拽绘制)
RoiEdit 编辑 ROI(选中、拖动、调整 Pin)
<controls:ImageDisplayControl
    SkiaImage="{Binding Image}"
    InteractionMode="{Binding InteractionMode}" />

XAML 基础用法

<controls:ImageDisplayControl
    SkiaImage="{Binding CurrentImage}"
    IsFitSize="True"
    Width="800"
    Height="600" />

代码中设置图像

using SkiaSharp;

// 从文件加载
using var stream = File.OpenRead("sample.jpg");
var skImage = SKImage.FromEncodedData(stream);
imageDisplay.SkiaImage = skImage;

// 适配显示
imageDisplay.FitSize();

显示源优先级

控件底图绘制遵循以下优先级:

  1. PixelBuffer 已分配 → 使用内部 SKBitmap 绘制(实时路径,视口裁剪)
  2. 否则 SkiaImage 非空 → 使用 SKImage 绘制(静态路径)
  3. 否则不绘制图像

PixelBufferFrameUpdated 事件会自动触发 InvalidateImageLayer();若直接写入 PixelBuffer.Bitmap 像素,需调用 PixelBuffer.NotifyUpdated()NotifyPixelBufferUpdated()

注意事项

  • SKImageImagePixelBuffer 的生命周期均由调用方管理,控件本身不会 Dispose 传入对象。
  • 实时采图请使用 PixelBuffer,避免每帧 SKImage.FromPixels / FromEncodedData 造成 GC 压力。
  • 控件最小尺寸为 64×64(MinWidth=64, MinHeight=64)。
  • 设置 IsFitSize=True 后,每次图像变更或控件尺寸变化都会自动重新适配。

3. Behaviors — 行为扩展

所有交互功能均通过 Avalonia XAML Behaviors (Xaml.Behaviors) 实现,以附加行为的方式组合到 ImageDisplayControl 上,保持控件核心简洁。

基本用法模板:

<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        
        <behaviors:ImageDragBehavior />
        <behaviors:ImageZoomBehavior />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>

3.1 ImageDragBehavior — 图像拖拽

命名空间: NjStdVision.Controls.Behaviors

通过鼠标左键拖拽平移图像视图。

属性
属性 类型 默认值 说明
IsEnabled bool true 是否启用拖拽(来自基类 Behavior
交互说明
操作 效果
鼠标左键按下并拖动 平移图像
XAML 示例
<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        <behaviors:ImageDragBehavior IsEnabled="{Binding IsDragEnabled}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>

3.2 ImageZoomBehavior — 图像缩放

命名空间: NjStdVision.Controls.Behaviors

通过鼠标滚轮以光标为中心缩放图像,中键单击恢复适配视图。

属性
属性 类型 默认值 说明
MinZoom double 0.01 最小缩放比例(1% = 图像原始尺寸的 1/100)
MaxZoom double 50.0 最大缩放比例(5000%)
ZoomStep double 1.1 每次滚轮的缩放步长倍率(1.1 = 每步缩放 10%)
MinVisibleSize double 30 缩放后图像在屏幕上的最小可见边长(像素),防止缩放过小
IsEnabled bool true 是否启用缩放
交互说明
操作 效果
鼠标滚轮向上 以光标位置为中心放大
鼠标滚轮向下 以光标位置为中心缩小
鼠标中键单击 调用 FitSize() 恢复适配视图
XAML 示例
<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        <behaviors:ImageZoomBehavior
            MinZoom="0.05"
            MaxZoom="20.0"
            ZoomStep="1.15" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>

3.3 RoiCreationBehavior — ROI 绘制

命名空间: NjStdVision.Controls.Behaviors

允许用户通过鼠标拖拽在图像上绘制新的 ROI(感兴趣区域)。

枚举:RoiCreationMode
说明
None 禁用创建模式
Rectangle 绘制矩形 ROI
Circle 绘制圆形 ROI
RotatedRectangle 绘制旋转矩形 ROI
Caliper 绘制卡尺 ROI(CaliperRoi,含探针预览)
Polygon 保留值(暂未实现拖拽创建)
属性
属性 类型 默认值 说明
CreationMode RoiCreationMode None 当前创建模式
TargetCollection ObservableCollection<RoiBase> 空集合 ROI 创建完成后添加到此集合
RoiCreatedCommand ICommand? null ROI 创建完成后执行的命令,参数为新建的 RoiBase
有效性约束
  • 矩形 ROI:宽度和高度均 > 5 像素(图像坐标)
  • 圆形 ROI:半径 > 5 像素
  • 旋转矩形:宽度和高度均 > 5 像素

绘制中的 ROI 以黄色边框实时预览。

XAML 示例
<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        <behaviors:RoiCreationBehavior
            CreationMode="{Binding CurrentCreationMode}"
            TargetCollection="{Binding Rois}"
            RoiCreatedCommand="{Binding OnRoiCreatedCommand}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>
ViewModel 示例
public RoiCreationMode CurrentCreationMode { get; set; } = RoiCreationMode.None;
public ObservableCollection<RoiBase> Rois { get; } = new();

// 切换到矩形绘制模式
public void StartDrawRectangle()
{
    CurrentCreationMode = RoiCreationMode.Rectangle;
}

// 停止绘制
public void StopDrawing()
{
    CurrentCreationMode = RoiCreationMode.None;
}

3.4 RoiInteractionBehavior — ROI 交互

命名空间: NjStdVision.Controls.Behaviors

最强大的 ROI 行为,支持 ROI 的完整交互:渲染、选中、拖动、通过 Pin 点调整形状、键盘删除。

属性
属性 类型 绑定模式 默认值 说明
Rois ObservableCollection<RoiBase> 单向 空集合 要交互的 ROI 集合
SelectedRoi RoiBase? 双向 null 当前选中的 ROI
RoiRemovedCommand ICommand? 单向 null ROI 被删除后执行的命令,参数为被删除的 RoiBase
IsEnabled bool true 是否启用交互
交互说明
操作 效果
左键单击 ROI 内部 选中该 ROI(显示控制点)
左键拖动 ROI 内部 移动整个 ROI
左键拖动 Pin 点 调整 ROI 形状/大小/旋转
左键单击空白区域 取消选中
Delete / Backspace 删除当前选中的 ROI
渲染层次
  1. 未选中的 ROI 先渲染
  2. 选中的 ROI 后渲染(在上层)
  3. 选中 ROI 的所有 Pin 控制点最后渲染
  4. 正在拖动的 Pin 以黄色高亮显示
XAML 示例
<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        <behaviors:ImageDragBehavior IsEnabled="{Binding !IsRoiMode}" />
        <behaviors:ImageZoomBehavior />
        <behaviors:RoiInteractionBehavior
            Rois="{Binding Rois}"
            SelectedRoi="{Binding SelectedRoi, Mode=TwoWay}"
            RoiRemovedCommand="{Binding OnRoiRemovedCommand}"
            IsEnabled="{Binding IsRoiMode}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>
与 RoiCreationBehavior 配合

同时附加 RoiCreationBehaviorRoiInteractionBehavior,并指向同一个 Rois 集合:

<behaviors:RoiCreationBehavior
    CreationMode="{Binding CreationMode}"
    TargetCollection="{Binding Rois}" />
<behaviors:RoiInteractionBehavior
    Rois="{Binding Rois}"
    SelectedRoi="{Binding SelectedRoi, Mode=TwoWay}" />

注意: 建议在绘制模式时通过 IsEnabled 禁用 ImageDragBehavior,避免拖拽冲突。


3.5 RoiRenderBehavior — ROI 只读渲染

命名空间: NjStdVision.Controls.Behaviors

纯渲染行为,将 ROI 集合叠加显示在图像上,不响应任何鼠标交互。适合预览窗格、结果展示等只读场景。

属性
属性 类型 默认值 说明
Rois ObservableCollection<RoiBase> 空集合 要渲染的 ROI 集合
XAML 示例

<controls:ImageDisplayControl SkiaImage="{Binding ResultImage}">
    <ix:Interaction.Behaviors>
        <behaviors:ImageZoomBehavior />
        <behaviors:RoiRenderBehavior Rois="{Binding Rois}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>

3.6 DetectionResultBehavior — 检测结果叠加

命名空间: NjStdVision.Controls.Behaviors

在图像上叠加显示检测结果(带标签和置信度的矩形框),并支持点击选中。

属性
属性 类型 绑定模式 默认值 说明
Results ObservableCollection<DetectionResult>? 单向 null 要渲染的检测结果集合
SelectedResult DetectionResult? 双向 null 当前选中的检测结果
IsEnabled bool true 是否启用交互
渲染特性
  • 每个 DetectionResult 以彩色矩形框显示,选中时变为不同颜色
  • 标签文字显示在矩形框左上角,带半透明黑色背景
  • 置信度 > 0 时自动附加百分比格式(如 Person (95%)
  • 线宽和字体大小随缩放自动调整,视觉效果保持一致
XAML 示例
<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        <behaviors:ImageZoomBehavior />
        <behaviors:ImageDragBehavior />
        <behaviors:DetectionResultBehavior
            Results="{Binding DetectionResults}"
            SelectedResult="{Binding SelectedResult, Mode=TwoWay}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>
ViewModel 示例
public ObservableCollection<DetectionResult> DetectionResults { get; } = new();

public void ShowResults(IEnumerable<(Rect box, string label, double score)> detections)
{
    DetectionResults.Clear();
    foreach (var (box, label, score) in detections)
    {
        DetectionResults.Add(new DetectionResult
        {
            Region     = box,
            Label      = label,
            Confidence = score,
            StrokeBrush = Brushes.LimeGreen
        });
    }
}

3.7 OptimizedImagePixelSamplerBehavior — 像素采样

命名空间: NjStdVision.Controls.Behaviors

在鼠标移动时(配合修饰键)实时采样光标下的像素颜色,通过命令将 PixelInfo 回传给 ViewModel。内部通过 PixelSampler 统一读取 PixelBufferSkiaImage 像素。

推荐: 若已附加 ImageInteractionFeedbackBehavior,状态栏请绑定 ViewportStatus不要再叠加本 Behavior(避免重复采样)。本 Behavior 适用于需要 Alt+修饰键触发、或 ViewModel 通过 ICommand 接收像素的独立场景。

属性
属性 类型 默认值 说明
PixelSampledCommand ICommand? null 采样完成后执行的命令,参数为 PixelInfo
TriggerKey KeyModifiers Alt 触发采样所需的修饰键(None/Alt/Control/Shift
SampleInterval int 16 采样节流间隔(毫秒),避免过于频繁的采样
工作流程
  1. 鼠标在控件上移动,且 HasImage == true
  2. 检查是否按下了 TriggerKey 修饰键
  3. 节流检查(距上次采样是否超过 SampleInterval 毫秒)
  4. 调用 PixelSampler.Sample:屏幕坐标 → 图像坐标,读取像素颜色
  5. 执行 PixelSampledCommand,传入 PixelInfo
XAML 示例
<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        <behaviors:OptimizedImagePixelSamplerBehavior
            TriggerKey="Alt"
            SampleInterval="32"
            PixelSampledCommand="{Binding OnPixelSampledCommand}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>
ViewModel 示例
using NjStdVision.Controls.Models;
using ReactiveUI;

public PixelInfo? CurrentPixel { get; private set; }

public ReactiveCommand<PixelInfo, Unit> OnPixelSampledCommand { get; }

public MyViewModel()
{
    OnPixelSampledCommand = ReactiveCommand.Create<PixelInfo>(info =>
    {
        CurrentPixel = info;
        // 更新状态栏信息
        StatusText = info.IsValid
            ? $"({info.ImagePosition.X:F0}, {info.ImagePosition.Y:F0})  {info.RgbString}"
            : "光标超出图像范围";
    });
}

3.8 ImageInteractionFeedbackBehavior — 光标与状态栏

命名空间: NjStdVision.Controls.Behaviors

配合 RoiInteractionBehavior 使用,根据 InteractionMode 与命中结果切换光标,并通过 PixelSampler 更新 ViewportStatus 供状态栏绑定。兼容 PixelBufferSkiaImage 两种显示源。

属性
属性 类型 默认值 说明
StatusUpdateInterval int 32 状态栏更新节流间隔(毫秒)
光标状态(ImageCursorKind
模式 / 命中 光标
Navigate 悬停 Hand / HandGrabbing(拖动中)
RoiEdit 命中 ROI 内部 Move
RoiEdit 命中 Pin Resize / Crosshair
RoiCreate Crosshair
XAML 示例
<controls:ImageDisplayControl
    x:Name="ImageViewer"
    SkiaImage="{Binding Image}"
    InteractionMode="{Binding InteractionMode}">
    <ix:Interaction.Behaviors>
        <behaviors:ImageDragBehavior />
        <behaviors:ImageZoomBehavior />
        <behaviors:ImageInteractionFeedbackBehavior />
        <behaviors:RoiInteractionBehavior Rois="{Binding Rois}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>

<TextBlock Text="{Binding #ImageViewer.ViewportStatus.Summary}" />

4. ROI 类型详解

所有 ROI 类型继承自 RoiBase,坐标均使用图像坐标系(像素为单位)。

RoiBase 公共成员

public abstract class RoiBase : INotifyPropertyChanged
{
    public string Id { get; set; }
    public string Name { get; set; }
    public bool IsSelected { get; set; }
    public bool IsHovered { get; set; }
    public IBrush StrokeBrush { get; set; }
    public IBrush FillBrush { get; set; }
    public double StrokeThickness { get; set; }
    public Rect? ImageBounds { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;
    public event EventHandler? GeometryChanged;

    public abstract List<RoiPin> GetPins();
    public abstract void Render(DrawingContext context, Matrix t);
    public abstract Rect GetBoundingRect();
    public abstract bool HitTest(Point point);
    public abstract void Move(Vector delta);
    public abstract void UpdatePin(RoiPin pin, Point newPosition);
    public abstract RoiBase Clone();
}

RoiInteractionBehavior / RoiRenderBehavior 订阅 GeometryChanged,自动触发 InvalidateImageRegion 局部刷新。


4.1 RectangleRoi — 矩形 ROI

// 创建
var roi = new RectangleRoi(new Rect(100, 100, 200, 150));

// 属性
roi.Bounds         // Rect:矩形的位置和尺寸(图像坐标)
roi.StrokeBrush    = Brushes.Blue;
roi.Name           = "检测区域1";

// 读取数据
double x      = roi.Bounds.X;
double y      = roi.Bounds.Y;
double width  = roi.Bounds.Width;
double height = roi.Bounds.Height;

控制点(8个): 4 个角点(Corner)+ 4 个边中点(Edge)


4.2 CircleRoi — 圆形 ROI

// 创建(圆心 + 半径)
var roi = new CircleRoi(new Point(300, 200), 80);

// 属性
roi.Center    // Point:圆心坐标(图像坐标)
roi.Radius    // double:半径(像素)

// 读取数据
double cx = roi.Center.X;
double cy = roi.Center.Y;
double r  = roi.Radius;

控制点(5个): 1 个中心点(Center)+ 4 个边缘点(Edge,上下左右)


4.3 RotatedRectangleRoi — 旋转矩形 ROI

// 创建(中心点, 宽, 高, 旋转角度(弧度))
var roi = new RotatedRectangleRoi(
    center: new Point(400, 300),
    width:  200,
    height: 100,
    angle:  Math.PI / 6  // 30°
);

// 属性
roi.Center   // Point:中心坐标(图像坐标)
roi.Width    // double:宽度(像素)
roi.Height   // double:高度(像素)
roi.Angle    // double:旋转角度(弧度,顺时针为正)

// 获取四个角点的世界坐标
// (通过继承的 GetPins() 中 PinType.Corner 的 Position 获取)

控制点: 4 个角点(Corner)+ 1 个旋转控制点(Rotation)


4.4 CaliperRoi — 卡尺 ROI

继承 RotatedRectangleRoi,用于边缘/卡尺测量。沿宽度分布青色探针虚线,沿高度(黄色指示线、Phi 测量方向)显示金色扫描箭头。

var roi = new CaliperRoi(new Point(400, 300), width: 320, height: 60, angle: 0)
{
    Name = "Caliper-1",
    ProbeCount = 20,
    StrokeBrush = Brushes.DodgerBlue,
};
属性 说明
Center / Width / Height / Angle RotatedRectangleRoi
ProbeCount 探针预览线数量(2–64,默认 20)

坐标约定: Width = 探针分布(Length2),Height = 扫描方向(Length1)。算法参数转换(如 CaliperParam)放在业务项目,参考 MeasureTestApp/Helpers/CaliperParamConverter.cs

4.5 RoiPin — 控制点

RoiPin 是 ROI 上的交互控制点,由 RoiManager 自动渲染和处理拖拽。

public class RoiPin
{
    public string Id { get; set; }       // 唯一标识
    public Point Position { get; set; }  // 图像坐标位置
    public PinType Type { get; set; }    // Corner / Edge / Center / Rotation / Custom
    public int Index { get; set; }       // 在 ROI 中的索引
    public double Size { get; set; }     // 显示尺寸(屏幕像素,默认 8)
    public IBrush Brush { get; set; }    // 颜色(默认蓝色)
}

// PinType 枚举
public enum PinType
{
    Corner,    // 角点(调整尺寸)
    Edge,      // 边中点(单轴调整)
    Center,    // 中心点(移动)
    Rotation,  // 旋转控制点
    Custom     // 自定义
}

5. 数据模型

5.1 DetectionResult — 检测结果

public class DetectionResult
{
    // 数据
    public Rect Region { get; set; }           // 检测框(图像坐标)
    public string? Label { get; set; }         // 类别标签
    public double Confidence { get; set; }     // 置信度(0.0~1.0,为 0 时不显示)
    public object? Tag { get; set; }           // 自定义附加数据
    public bool IsSelected { get; set; }       // 是否被选中

    // 样式(正常状态)
    public IBrush StrokeBrush { get; set; }          // 边框颜色(默认红色)
    public IBrush? FillBrush { get; set; }           // 填充颜色(默认半透明红色)

    // 样式(选中状态)
    public IBrush SelectedStrokeBrush { get; set; }  // 选中边框颜色(默认绿色)
    public IBrush? SelectedFillBrush { get; set; }   // 选中填充颜色(默认半透明绿色)

    public double StrokeThickness { get; set; }      // 边框粗细(默认 2)

    // 方法
    public void Render(DrawingContext context, Matrix transform);  // 渲染
    public bool HitTest(Point point);                              // 点击测试
}

创建示例:

var result = new DetectionResult
{
    Region     = new Rect(150, 200, 100, 80),
    Label      = "螺钉缺陷",
    Confidence = 0.96,
    StrokeBrush         = Brushes.OrangeRed,
    SelectedStrokeBrush = Brushes.Cyan,
    Tag        = myDefectObject  // 关联业务对象
};

5.2 PixelInfo — 像素信息

由内部 PixelSampler 采样产生,通过 ViewportStatus.PixelOptimizedImagePixelSamplerBehaviorPixelSampledCommand 传递。兼容 PixelBufferSkiaImage

public class PixelInfo
{
    public Point ScreenPosition { get; set; }  // 控件坐标(屏幕像素)
    public Point ImagePosition { get; set; }   // 图像坐标(图像像素)
    public SKColor Color { get; set; }         // SkiaSharp 颜色值
    public bool IsValid { get; set; }          // 坐标是否在图像范围内

    // 便捷属性
    public string RgbString { get; }   // "RGB(R, G, B)"  或 "N/A"
    public string HexString { get; }   // "#RRGGBB"       或 "N/A"
    public byte Alpha { get; }         // Alpha 通道值
}

使用示例:

void OnPixelSampled(PixelInfo info)
{
    if (!info.IsValid) return;

    Console.WriteLine($"图像坐标: ({info.ImagePosition.X:F0}, {info.ImagePosition.Y:F0})");
    Console.WriteLine($"颜色: {info.RgbString}  ({info.HexString})");
    Console.WriteLine($"Alpha: {info.Alpha}");
    Console.WriteLine($"灰度: {(info.Color.Red + info.Color.Green + info.Color.Blue) / 3}");
}

5.3 ImageViewportStatus — 视口状态

ImageInteractionFeedbackBehavior 写入,通过 ImageDisplayControl.ViewportStatus 绑定到状态栏。

public class ImageViewportStatus
{
    public double ZoomPercent { get; init; }
    public Point ImagePosition { get; init; }
    public PixelInfo? Pixel { get; init; }
    public string Summary { get; }  // 如 "(120, 340) | RGB(…) | 缩放 150%"
    public static ImageViewportStatus Empty { get; }
}

ViewModel 订阅像素(无需额外 Behavior):

imageViewer.GetObservable(ImageDisplayControl.ViewportStatusProperty)
    .Select(s => s.Pixel)
    .Subscribe(pixel => CurrentPixel = pixel);

5.4 ImagePixelBuffer — 可复用像素缓冲区

命名空间: NjStdVision.Controls.Models

供实时采图、算法回写后原地刷新显示的可复用缓冲区。内部以 SKBitmap 为绘制源,避免每帧创建 SKImage

属性
属性 类型 说明
Width / Height int 当前位图尺寸
ColorType SKColorType 像素格式(默认 Bgra8888
RowBytes int 行字节数(stride)
FrameId ulong 帧序号,原子递增,跨线程安全
Bitmap SKBitmap 绘制/写入用的位图(未分配时抛异常)
IsAllocated bool 是否已分配有效位图
事件
事件 说明
FrameUpdated 帧更新时触发;ImageDisplayControl 订阅后自动 InvalidateImageLayer()
方法
方法 说明
TryGetBitmap() 原子获取当前位图引用,供渲染线程安全使用
EnsureSize(w, h, colorType) 确保尺寸与格式;变化时重建缓冲区
AttachBytes(buffer, w, h, rowBytes, …) 绑定外部 pinned byte[](零拷贝显示)
AttachPointer(ptr, w, h, rowBytes, …) 绑定外部非托管像素指针(零拷贝)
CopyFrom(IntPtr/ReadOnlySpan<byte>, …) 复制到内部自有缓冲区(适合相机回调写入池化 byte[]
ImportFrom(SKImage, …) 将静态 SKImage 导入为自有缓冲区(非实时路径)
NotifyUpdated() 外部已直接写入 Bitmap 像素后调用,递增 FrameId 并触发 FrameUpdated
Dispose() 释放位图与外部 GCHandle
线程安全策略
  • FrameId 使用 Interlocked 原子递增
  • 位图重建采用延迟释放RetireBitmap):旧位图在下一帧重建时才释放,避免渲染线程 use-after-free
  • 工作线程写入像素后调用 NotifyUpdated();UI 线程通过 FrameUpdated 自动重绘
典型用法
using NjStdVision.Controls.Models;
using SkiaSharp;

// 1. 创建并绑定到控件(ViewModel 持有生命周期)
var buffer = new ImagePixelBuffer(1920, 1080);
imageDisplay.PixelBuffer = buffer;
imageDisplay.IsFitSize = true;

// 2a. 相机回调:复制到池化缓冲区
buffer.CopyFrom(nativePtr, width, height, rowBytes, SKColorType.Bgra8888);
// CopyFrom 内部已调用 NotifyUpdated()

// 2b. 零拷贝:绑定 pinned byte[]
buffer.AttachBytes(pooledBytes, width, height, rowBytes);

// 2c. 算法直接写 Bitmap 像素
unsafe
{
    var ptr = (byte*)buffer.Bitmap.GetPixels();
    // ... 写入像素 ...
}
buffer.NotifyUpdated();

// 3. 静态图一次性导入(替代 SkiaImage 绑定)
buffer.ImportFrom(SKImage.FromEncodedData(stream));
imageDisplay.PixelBuffer = buffer;
与 SkiaImage 的选择
场景 推荐
文件浏览、离线检测、单次加载 SkiaImage
相机实时预览、连续采图 ImagePixelBuffer
算法原地回写显示帧 ImagePixelBuffer.CopyFrom 或直接写 Bitmap

6. 转换器 (Converters)

SkiaImageValueConverter

将文件路径字符串或 avares:// 资源路径转换为 SKImage

public class SkiaImageValueConverter : IValueConverter
{
    // 支持输入类型:string(文件路径 或 avares:// URI)、SKImage
    // 输出类型:SKImage?
}

XAML 注册和使用:


<UserControl.Resources>
    <converters:SkiaImageValueConverter x:Key="SkiaConverter" />
</UserControl.Resources>


<controls:ImageDisplayControl
    SkiaImage="{Binding ImagePath, Converter={StaticResource SkiaConverter}}" />


<controls:ImageDisplayControl
    SkiaImage="{Binding Source='avares://MyApp/Assets/logo.png',
                Converter={StaticResource SkiaConverter}}" />

SkiaImageTypeConverter

类型转换器(TypeConverter),用于在 XAML 属性中直接使用字符串路径(目前已注释,待后续启用)。


7. 接口 (Interfaces)

渲染图层

public enum RenderLayer { Image, Overlay }

底图变化用 InvalidateImageLayer(),标注层用 InvalidateOverlay()InvalidateImageRegion()

IRenderable / IOverlayRenderable

public interface IRenderable
{
    void Render(DrawingContext context, Matrix transform);
}

public interface IOverlayRenderable : IRenderable;

推荐新叠加层实现 IOverlayRenderable;数据变化时调用 AssociatedObject?.InvalidateOverlay()

自定义渲染 Behavior 示例:

using Avalonia;
using Avalonia.Media;
using Avalonia.Xaml.Interactivity;
using NjStdVision.Controls;
using NjStdVision.Controls.Interface;

public class CrosshairBehavior : Behavior<ImageDisplayControl>, IRenderable
{
    public void Render(DrawingContext context, Matrix transform)
    {
        var img = AssociatedObject?.SkiaImage;
        if (img == null) return;

        var pen = new Pen(Brushes.Yellow, 1.0 / transform.M11);
        var cx = img.Width / 2.0;
        var cy = img.Height / 2.0;

        // 水平线
        context.DrawLine(pen, new Point(0, cy), new Point(img.Width, cy));
        // 垂直线
        context.DrawLine(pen, new Point(cx, 0), new Point(cx, img.Height));
    }
}
<controls:ImageDisplayControl SkiaImage="{Binding Image}">
    <ix:Interaction.Behaviors>
        <local:CrosshairBehavior />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>

8. 典型使用场景示例

8.1 场景一:基础图像浏览器(拖拽+缩放)

<controls:ImageDisplayControl
    SkiaImage="{Binding Image}"
    IsFitSize="True">
    <ix:Interaction.Behaviors>
        <behaviors:ImageDragBehavior />
        <behaviors:ImageZoomBehavior
            MinZoom="0.02"
            MaxZoom="30.0"
            ZoomStep="1.1" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>
// ViewModel
public class ImageViewerViewModel
{
    public SKImage? Image { get; private set; }

    public void LoadImage(string path)
    {
        Image?.Dispose();
        using var stream = File.OpenRead(path);
        Image = SKImage.FromEncodedData(stream);
    }
}

8.2 场景二:ROI 编辑工具

<DockPanel>
    
    <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="4">
        <Button Content="移动"      Command="{Binding SetModeNoneCommand}" />
        <Button Content="画矩形"    Command="{Binding SetModeRectCommand}" />
        <Button Content="画圆形"    Command="{Binding SetModeCircleCommand}" />
        <Button Content="旋转矩形"  Command="{Binding SetModeRotatedRectCommand}" />
        <Button Content="清除所有"  Command="{Binding ClearRoisCommand}" />
    </StackPanel>

    
    <controls:ImageDisplayControl SkiaImage="{Binding Image}" IsFitSize="True">
        <ix:Interaction.Behaviors>
            
            <behaviors:ImageDragBehavior
                IsEnabled="{Binding IsViewMode}" />
            <behaviors:ImageZoomBehavior />
            
            <behaviors:RoiCreationBehavior
                CreationMode="{Binding CreationMode}"
                TargetCollection="{Binding Rois}"
                RoiCreatedCommand="{Binding OnRoiCreatedCommand}" />
            
            <behaviors:RoiInteractionBehavior
                Rois="{Binding Rois}"
                SelectedRoi="{Binding SelectedRoi, Mode=TwoWay}"
                RoiRemovedCommand="{Binding OnRoiRemovedCommand}"
                IsEnabled="{Binding IsViewMode}" />
        </ix:Interaction.Behaviors>
    </controls:ImageDisplayControl>
</DockPanel>
public class RoiEditorViewModel : ReactiveObject
{
    public ObservableCollection<RoiBase> Rois { get; } = new();

    [Reactive] public RoiCreationMode CreationMode { get; set; } = RoiCreationMode.None;
    [Reactive] public RoiBase? SelectedRoi { get; set; }

    public bool IsViewMode => CreationMode == RoiCreationMode.None;

    public void SetModeRect()    => CreationMode = RoiCreationMode.Rectangle;
    public void SetModeCircle()  => CreationMode = RoiCreationMode.Circle;
    public void SetModeRotated() => CreationMode = RoiCreationMode.RotatedRectangle;
    public void SetModeNone()    => CreationMode = RoiCreationMode.None;

    public void ClearRois() => Rois.Clear();

    public void OnRoiCreated(RoiBase roi)
    {
        roi.Name = $"ROI-{Rois.Count}";
        // 自动切回查看模式
        CreationMode = RoiCreationMode.None;
    }

    public void OnRoiRemoved(RoiBase roi)
    {
        Console.WriteLine($"已删除: {roi.Name}");
    }
}

8.3 场景三:检测结果可视化

<controls:ImageDisplayControl SkiaImage="{Binding ResultImage}" IsFitSize="True">
    <ix:Interaction.Behaviors>
        <behaviors:ImageDragBehavior />
        <behaviors:ImageZoomBehavior />
        <behaviors:DetectionResultBehavior
            Results="{Binding DetectionResults}"
            SelectedResult="{Binding SelectedResult, Mode=TwoWay}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>


<ListBox Items="{Binding DetectionResults}"
         SelectedItem="{Binding SelectedResult, Mode=TwoWay}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal" Spacing="8">
                <TextBlock Text="{Binding Label}" />
                <TextBlock Text="{Binding Confidence, StringFormat={}{0:P0}}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
public class DetectionViewModel
{
    public ObservableCollection<DetectionResult> DetectionResults { get; } = new();
    public DetectionResult? SelectedResult { get; set; }

    public void RunDetection(SKImage image)
    {
        // 假设 detector 返回 (Rect box, string label, float score)[]
        var detections = myDetector.Detect(image);

        DetectionResults.Clear();
        foreach (var d in detections)
        {
            DetectionResults.Add(new DetectionResult
            {
                Region      = d.box,
                Label       = d.label,
                Confidence  = d.score,
                StrokeBrush = d.score > 0.9 ? Brushes.LimeGreen : Brushes.OrangeRed
            });
        }
    }
}

8.4 场景四:像素采样(颜色拾取)

方式 A(推荐): 使用 ImageInteractionFeedbackBehavior + ViewportStatus 绑定状态栏,鼠标移动即更新坐标与 RGB。

<DockPanel>
    <Border DockPanel.Dock="Bottom" Background="#222" Padding="8,4">
        <TextBlock Text="{Binding #ImageViewer.ViewportStatus.Summary}" Foreground="White" />
    </Border>

    <controls:ImageDisplayControl
        x:Name="ImageViewer"
        SkiaImage="{Binding Image}"
        IsFitSize="True">
        <ix:Interaction.Behaviors>
            <behaviors:ImageDragBehavior />
            <behaviors:ImageZoomBehavior />
            <behaviors:ImageInteractionFeedbackBehavior />
        </ix:Interaction.Behaviors>
    </controls:ImageDisplayControl>
</DockPanel>

方式 B: 按住修饰键才采样,通过 ICommand 回传 ViewModel(独立工具,勿与方式 A 叠加)。

<controls:ImageDisplayControl SkiaImage="{Binding Image}" IsFitSize="True">
    <ix:Interaction.Behaviors>
        <behaviors:ImageDragBehavior />
        <behaviors:ImageZoomBehavior />
        <behaviors:OptimizedImagePixelSamplerBehavior
            TriggerKey="Alt"
            SampleInterval="16"
            PixelSampledCommand="{Binding OnPixelSampledCommand}" />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>
public class PixelSamplerViewModel
{
    public string PixelCoord { get; private set; } = "—";
    public string PixelRgb   { get; private set; } = "—";
    public string PixelHex   { get; private set; } = "#000000";

    public ICommand OnPixelSampledCommand { get; }

    public PixelSamplerViewModel()
    {
        OnPixelSampledCommand = ReactiveCommand.Create<PixelInfo>(OnPixelSampled);
    }

    private void OnPixelSampled(PixelInfo info)
    {
        if (!info.IsValid)
        {
            PixelCoord = "超出范围";
            return;
        }

        PixelCoord = $"({info.ImagePosition.X:F0}, {info.ImagePosition.Y:F0})";
        PixelRgb   = info.RgbString;
        PixelHex   = info.HexString;
    }
}

8.5 场景五:完整工业视觉检测台

综合所有 Behavior,构建一个包含图像浏览、ROI 设置、检测结果展示和状态栏的完整检测工作台:

<Grid ColumnDefinitions="*,280" RowDefinitions="*,Auto">
    <controls:ImageDisplayControl
        x:Name="ImageViewer"
        Grid.Row="0" Grid.Column="0"
        SkiaImage="{Binding CurrentImage}"
        PixelBuffer="{Binding LiveBuffer}"
        InteractionMode="{Binding InteractionMode}"
        IsFitSize="True"
        Focusable="True">
        <ix:Interaction.Behaviors>
            <behaviors:ImageDragBehavior IsEnabled="{Binding IsViewMode}" />
            <behaviors:ImageZoomBehavior />
            <behaviors:ImageInteractionFeedbackBehavior />
            <behaviors:RoiCreationBehavior
                CreationMode="{Binding CreationMode}"
                TargetCollection="{Binding Rois}" />
            <behaviors:RoiInteractionBehavior
                Rois="{Binding Rois}"
                SelectedRoi="{Binding SelectedRoi, Mode=TwoWay}"
                IsEnabled="{Binding IsViewMode}" />
            <behaviors:DetectionResultBehavior
                Results="{Binding Results}"
                SelectedResult="{Binding SelectedResult, Mode=TwoWay}" />
        </ix:Interaction.Behaviors>
    </controls:ImageDisplayControl>

    <Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
            Background="#222" Padding="8,4">
        <TextBlock Text="{Binding #ImageViewer.ViewportStatus.Summary}" Foreground="White" />
    </Border>

    <StackPanel Grid.Row="0" Grid.Column="1" Spacing="8" Margin="8">
        <TextBlock Text="检测结果" FontWeight="Bold" />
        <ListBox Items="{Binding Results}"
                 SelectedItem="{Binding SelectedResult, Mode=TwoWay}"
                 MaxHeight="200" />
        <Separator />
        <TextBlock Text="ROI 列表" FontWeight="Bold" />
        <ListBox Items="{Binding Rois}"
                 SelectedItem="{Binding SelectedRoi, Mode=TwoWay}"
                 MaxHeight="200" />
    </StackPanel>
</Grid>

状态栏通过 ImageInteractionFeedbackBehavior 更新,无需 OptimizedImagePixelSamplerBehavior


8.6 场景六:实时采图显示(PixelBuffer)

相机回调在工作线程写入缓冲区,控件在 UI 线程自动重绘:

public sealed class CameraDisplayViewModel : IDisposable
{
    private readonly ImagePixelBuffer _buffer = new(1920, 1080);

    public ImagePixelBuffer LiveBuffer => _buffer;

    // 相机回调(非 UI 线程)
    public void OnFrameReceived(IntPtr data, int width, int height, int stride)
    {
        _buffer.CopyFrom(data, width, height, stride, SKColorType.Bgra8888);
        // CopyFrom → NotifyUpdated → FrameUpdated → InvalidateImageLayer
    }

    public void Dispose() => _buffer.Dispose();
}
<controls:ImageDisplayControl
    PixelBuffer="{Binding LiveBuffer}"
    IsFitSize="True">
    <ix:Interaction.Behaviors>
        <behaviors:ImageDragBehavior />
        <behaviors:ImageZoomBehavior />
        <behaviors:ImageInteractionFeedbackBehavior />
    </ix:Interaction.Behaviors>
</controls:ImageDisplayControl>

零拷贝路径(调用方保持缓冲区存活):

// 池化 byte[] 在整段显示期间保持 pinned
_buffer.AttachBytes(pooledFrame, width, height, rowBytes);

切换显示源: 离线加载用 SkiaImage,开始实时预览时设置 PixelBuffer 并清空 SkiaImage(或反之)。


9. 架构与扩展

设计原则

ImageDisplayControl
    ├── RenderImageLayer              ← SkiaImageDrawOp(SKBitmap 实时 / SKImage 静态)+ 视口裁剪
    ├── RenderOverlayLayer            ← IOverlayRenderable Behaviors + 局部脏区 PushClip
    ├── PixelBuffer / SkiaImage       ← 双显示源,PixelBuffer 优先
    └── Behaviors
         ├── ImageDragBehavior                  ← 平移(Navigate 模式)
         ├── ImageZoomBehavior                  ← 缩放(含 MinVisibleSize 限制)
         ├── ImageInteractionFeedbackBehavior   ← 光标 + ViewportStatus(PixelSampler)
         ├── RoiCreationBehavior                ← ROI 创建(含 Caliper)
         ├── RoiInteractionBehavior             ← ROI 交互(监听 PixelBuffer 尺寸变化)
         ├── RoiRenderBehavior                  ← ROI 只读渲染
         ├── DetectionResultBehavior
         └── OptimizedImagePixelSamplerBehavior ← 可选;勿与 Feedback 叠加

IRenderable 扩展机制

ImageDisplayControl.Render() 会遍历所有附加的 Behavior,凡实现了 IRenderable 接口的均会被调用,因此可以无限扩展自定义叠加层

// 自定义:在图像上绘制测量标尺
public class RulerOverlayBehavior : Behavior<ImageDisplayControl>, IRenderable
{
    public void Render(DrawingContext context, Matrix transform)
    {
        // 在此使用图像坐标系绘制任意内容
        // transform.M11 == 当前缩放比例,可用于保持线宽恒定
    }
}

9.1 业务项目扩展(算法叠加层)

放在 NjStdVision.Controls 放在业务项目
CaliperRoiImageDisplayControlImagePixelBuffer CaliperParamConverterFrameBufferPool
IOverlayRenderable MeasureEdgeOverlayBehavior
RoiInteractionBehaviorPixelSampler MeasurePreviewBehavior、相机回调编排
public class MyOverlayBehavior : Behavior<ImageDisplayControl>, IOverlayRenderable
{
    public void Render(DrawingContext context, Matrix transform) { }
}

完整范例见 MeasureTestApp

9.2 显示路径选择(SkiaImage vs PixelBuffer)

操作 SkiaImage ImagePixelBuffer
底图重绘 InvalidateImageLayer() NotifyUpdated() / FrameUpdated 自动触发
像素采样 PeekPixels 或可读位图缓存 直接 Bitmap.GetPixel
视口裁剪 GetVisibleImageRect() 同左
ROI 边界 ImageBounds 自动更新 PixelBuffer 变更时 RoiInteractionBehavior 同步 roi.ImageBounds
线程模型 UI 线程赋值 工作线程写入 + NotifyUpdated,UI 自动刷新

反模式(实时路径):

  • 每帧 SKImage.FromEncodedData / FromPixels
  • 每帧 new byte[w*h*channels]
  • 同时附加 ImageInteractionFeedbackBehaviorOptimizedImagePixelSamplerBehavior

坐标系说明

坐标系 说明 用途
屏幕坐标 控件的 CSS 像素坐标,左上角为原点 鼠标事件原始坐标
图像坐标 图像像素坐标,左上角为原点 ROI、检测结果数据存储

屏幕坐标 → 图像坐标:

var imagePoint = imageDisplayControl.ScreenToImagePoint(screenPoint);

图像坐标 → 屏幕坐标:

var screenPoint = imageDisplayControl.ImageToScreenPoint(imagePoint);

10. 常见问题

Q: 图像显示为空白 / 不显示

  • 检查 HasImage 是否为 truePixelBuffer.IsAllocatedSkiaImage != null
  • 若使用 PixelBuffer,确认已调用 EnsureSize / CopyFrom / AttachBytes 等分配方法
  • 若直接写入 Bitmap 像素,确认已调用 NotifyUpdated()
  • 检查 SKImage / ImagePixelBuffer 是否在显示前已被 Dispose
  • 确认控件尺寸不为 0(Width/Height 已设置或父容器有适当约束)
  • 若使用 IsFitSize,确保控件 Bounds 已初始化(可在 Loaded 事件后设置图像)

Q: ROI 无法拖动

  • 确认 ImageDragBehaviorIsEnabled 在 ROI 模式下已禁用(避免与 ROI 拖拽冲突)
  • 确认 RoiInteractionBehavior.IsEnabled = true
  • 确认控件可以获得焦点(Focusable=true,已在构造函数中设置)

Q: Delete 键无法删除 ROI

  • 控件需要获得焦点才能接收键盘事件,可在 PointerPressed 时调用 Focus(),或在 XAML 中设置 Focusable="True"

Q: 像素采样不工作

  • 若使用 ImageInteractionFeedbackBehavior:鼠标在控件内移动即更新 ViewportStatus,无需按修饰键
  • 若使用 OptimizedImagePixelSamplerBehavior:确认按住了正确的修饰键(默认 Alt
  • 确认 HasImage == true
  • 检查 SampleInterval / StatusUpdateInterval 是否过大
  • 不要同时叠加 ImageInteractionFeedbackBehaviorOptimizedImagePixelSamplerBehavior(重复采样且易混淆)

Q: 实时预览卡顿 / GC 压力大

  • 改用 ImagePixelBuffer + CopyFrom / AttachPointer,避免每帧创建 SKImage
  • 使用 Channel 容量 1–2 的 latest-frame-only 队列
  • 算法在 Task.Run / 工作线程执行,仅将 NotifyUpdated 结果刷新到 UI
  • 叠加层用 InvalidateImageRegion,不要每帧 InvalidateOverlay 全量重绘

Q: 多个 Behavior 的事件冲突

  • RoiCreationBehaviorImageDragBehavior 都监听鼠标左键,在创建模式下应禁用 Drag Behavior
  • 使用绑定控制 IsEnabled 属性实现互斥

Q: 如何导出 ROI 数据?

// 序列化矩形 ROI(示例)
var data = Rois
    .OfType<RectangleRoi>()
    .Select(r => new {
        r.Name,
        X      = r.Bounds.X,
        Y      = r.Bounds.Y,
        Width  = r.Bounds.Width,
        Height = r.Bounds.Height
    })
    .ToList();

var json = System.Text.Json.JsonSerializer.Serialize(data);

Q: 如何在高 DPI 屏幕上保证清晰度?

SkiaImageDrawOp 使用 SkiaSharp 直接绘制 SKImageSKBitmap,Avalonia 会自动处理 DPI 缩放。确保图像尺寸与原始像素尺寸一致,不要预先缩放。

Q: PixelBuffer 与 SkiaImage 能同时绑定吗?

可以绑定,但绘制时 PixelBuffer 优先。实时预览开始后建议将 SkiaImagenullDispose 旧图,避免混淆显示源与多余内存占用。


文档版本:v1.2 | 更新日期:2026-06-21 | 对应包版本:NjStdVision.Controls 1.0.2

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2 95 6/21/2026
1.0.1 94 6/20/2026
1.0.0 115 3/22/2026