NjStdVision.Controls
1.0.2
dotnet add package NjStdVision.Controls --version 1.0.2
NuGet\Install-Package NjStdVision.Controls -Version 1.0.2
<PackageReference Include="NjStdVision.Controls" Version="1.0.2" />
<PackageVersion Include="NjStdVision.Controls" Version="1.0.2" />
<PackageReference Include="NjStdVision.Controls" />
paket add NjStdVision.Controls --version 1.0.2
#r "nuget: NjStdVision.Controls, 1.0.2"
#:package NjStdVision.Controls@1.0.2
#addin nuget:?package=NjStdVision.Controls&version=1.0.2
#tool nuget:?package=NjStdVision.Controls&version=1.0.2
NjStdVision.Controls 详细使用文档
基于 Avalonia 11 + SkiaSharp 构建的工业视觉图像显示控件库
Target Framework:.NET 8.0| 支持静态SKImage与实时ImagePixelBuffer双显示路径
目录
- 快速开始
- ImageDisplayControl — 核心控件
- Behaviors — 行为扩展
- ROI 类型详解
- 数据模型
- 转换器 (Converters)
- 接口 (Interfaces)
- 典型使用场景示例
- 8.1 场景一:基础图像浏览器(拖拽+缩放)
- 8.2 场景二:ROI 编辑工具
- 8.3 场景三:检测结果可视化
- 8.4 场景四:像素采样(颜色拾取)
- 8.5 场景五:完整工业视觉检测台
- 8.6 场景六:实时采图显示(PixelBuffer)
- 架构与扩展
- 常见问题
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) |
获取当前图像尺寸(兼容 PixelBuffer 与 SkiaImage) |
ScreenToImagePoint(Point) |
屏幕坐标 → 图像坐标 |
ImageToScreenPoint(Point) |
图像坐标 → 屏幕坐标 |
ImageRegionToScreenRect(Rect) |
图像区域 → 屏幕包围矩形 |
GetVisibleImageRect() |
当前视口在图像坐标系中的可见区域(用于视口裁剪) |
InvalidateOverlay() |
请求重绘标注层(ROI、自定义叠加) |
InvalidateImageLayer() |
请求重绘底图层(图像或视口变换变化) |
NotifyPixelBufferUpdated() |
外部已写入 PixelBuffer 像素后请求底图重绘(等价于 InvalidateImageLayer) |
InvalidateImageRegion(Rect, padding) |
按图像坐标脏区请求标注层重绘(同帧合并) |
UpdateViewportStatus(Point, PixelInfo?) |
更新视口状态栏数据 |
ClearViewportStatus() |
清空视口状态(鼠标移出时) |
ImageInteractionMode
通过 InteractionMode 属性协调 ImageDragBehavior、RoiCreationBehavior、RoiInteractionBehavior 等行为的输入互斥:
| 值 | 说明 |
|---|---|
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();
显示源优先级
控件底图绘制遵循以下优先级:
PixelBuffer已分配 → 使用内部SKBitmap绘制(实时路径,视口裁剪)- 否则
SkiaImage非空 → 使用SKImage绘制(静态路径) - 否则不绘制图像
PixelBuffer 的 FrameUpdated 事件会自动触发 InvalidateImageLayer();若直接写入 PixelBuffer.Bitmap 像素,需调用 PixelBuffer.NotifyUpdated() 或 NotifyPixelBufferUpdated()。
注意事项
SKImage与ImagePixelBuffer的生命周期均由调用方管理,控件本身不会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 |
渲染层次
- 未选中的 ROI 先渲染
- 选中的 ROI 后渲染(在上层)
- 选中 ROI 的所有 Pin 控制点最后渲染
- 正在拖动的 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 配合
同时附加 RoiCreationBehavior 和 RoiInteractionBehavior,并指向同一个 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 统一读取 PixelBuffer 或 SkiaImage 像素。
推荐: 若已附加
ImageInteractionFeedbackBehavior,状态栏请绑定ViewportStatus,不要再叠加本 Behavior(避免重复采样)。本 Behavior 适用于需要 Alt+修饰键触发、或 ViewModel 通过ICommand接收像素的独立场景。
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
PixelSampledCommand |
ICommand? |
null |
采样完成后执行的命令,参数为 PixelInfo |
TriggerKey |
KeyModifiers |
Alt |
触发采样所需的修饰键(None/Alt/Control/Shift) |
SampleInterval |
int |
16 |
采样节流间隔(毫秒),避免过于频繁的采样 |
工作流程
- 鼠标在控件上移动,且
HasImage == true - 检查是否按下了
TriggerKey修饰键 - 节流检查(距上次采样是否超过
SampleInterval毫秒) - 调用
PixelSampler.Sample:屏幕坐标 → 图像坐标,读取像素颜色 - 执行
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 供状态栏绑定。兼容 PixelBuffer 与 SkiaImage 两种显示源。
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
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.Pixel 或 OptimizedImagePixelSamplerBehavior 的 PixelSampledCommand 传递。兼容 PixelBuffer 与 SkiaImage。
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 | 放在业务项目 |
|---|---|
CaliperRoi、ImageDisplayControl、ImagePixelBuffer |
CaliperParamConverter、FrameBufferPool |
IOverlayRenderable |
MeasureEdgeOverlayBehavior |
RoiInteractionBehavior、PixelSampler |
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] - 同时附加
ImageInteractionFeedbackBehavior与OptimizedImagePixelSamplerBehavior
坐标系说明
| 坐标系 | 说明 | 用途 |
|---|---|---|
| 屏幕坐标 | 控件的 CSS 像素坐标,左上角为原点 | 鼠标事件原始坐标 |
| 图像坐标 | 图像像素坐标,左上角为原点 | ROI、检测结果数据存储 |
屏幕坐标 → 图像坐标:
var imagePoint = imageDisplayControl.ScreenToImagePoint(screenPoint);
图像坐标 → 屏幕坐标:
var screenPoint = imageDisplayControl.ImageToScreenPoint(imagePoint);
10. 常见问题
Q: 图像显示为空白 / 不显示
- 检查
HasImage是否为true(PixelBuffer.IsAllocated或SkiaImage != null) - 若使用
PixelBuffer,确认已调用EnsureSize/CopyFrom/AttachBytes等分配方法 - 若直接写入
Bitmap像素,确认已调用NotifyUpdated() - 检查
SKImage/ImagePixelBuffer是否在显示前已被Dispose - 确认控件尺寸不为 0(
Width/Height已设置或父容器有适当约束) - 若使用
IsFitSize,确保控件Bounds已初始化(可在Loaded事件后设置图像)
Q: ROI 无法拖动
- 确认
ImageDragBehavior的IsEnabled在 ROI 模式下已禁用(避免与 ROI 拖拽冲突) - 确认
RoiInteractionBehavior.IsEnabled = true - 确认控件可以获得焦点(
Focusable=true,已在构造函数中设置)
Q: Delete 键无法删除 ROI
- 控件需要获得焦点才能接收键盘事件,可在
PointerPressed时调用Focus(),或在 XAML 中设置Focusable="True"
Q: 像素采样不工作
- 若使用
ImageInteractionFeedbackBehavior:鼠标在控件内移动即更新ViewportStatus,无需按修饰键 - 若使用
OptimizedImagePixelSamplerBehavior:确认按住了正确的修饰键(默认Alt) - 确认
HasImage == true - 检查
SampleInterval/StatusUpdateInterval是否过大 - 不要同时叠加
ImageInteractionFeedbackBehavior与OptimizedImagePixelSamplerBehavior(重复采样且易混淆)
Q: 实时预览卡顿 / GC 压力大
- 改用
ImagePixelBuffer+CopyFrom/AttachPointer,避免每帧创建SKImage - 使用
Channel容量 1–2 的 latest-frame-only 队列 - 算法在
Task.Run/ 工作线程执行,仅将NotifyUpdated结果刷新到 UI - 叠加层用
InvalidateImageRegion,不要每帧InvalidateOverlay全量重绘
Q: 多个 Behavior 的事件冲突
RoiCreationBehavior和ImageDragBehavior都监听鼠标左键,在创建模式下应禁用 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 直接绘制 SKImage 或 SKBitmap,Avalonia 会自动处理 DPI 缩放。确保图像尺寸与原始像素尺寸一致,不要预先缩放。
Q: PixelBuffer 与 SkiaImage 能同时绑定吗?
可以绑定,但绘制时 PixelBuffer 优先。实时预览开始后建议将 SkiaImage 置 null 并 Dispose 旧图,避免混淆显示源与多余内存占用。
文档版本:v1.2 | 更新日期:2026-06-21 | 对应包版本:NjStdVision.Controls 1.0.2
| Product | Versions 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. |
-
net8.0
- Avalonia (>= 11.3.8)
- Avalonia.Skia (>= 11.3.8)
- System.Reactive (>= 6.1.0)
- Xaml.Behaviors (>= 11.3.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.