NjStdVision.Controls
1.0.1
See the version list below for details.
dotnet add package NjStdVision.Controls --version 1.0.1
NuGet\Install-Package NjStdVision.Controls -Version 1.0.1
<PackageReference Include="NjStdVision.Controls" Version="1.0.1" />
<PackageVersion Include="NjStdVision.Controls" Version="1.0.1" />
<PackageReference Include="NjStdVision.Controls" />
paket add NjStdVision.Controls --version 1.0.1
#r "nuget: NjStdVision.Controls, 1.0.1"
#:package NjStdVision.Controls@1.0.1
#addin nuget:?package=NjStdVision.Controls&version=1.0.1
#tool nuget:?package=NjStdVision.Controls&version=1.0.1
NjStdVision.Controls 详细使用文档
基于 Avalonia 11 + SkiaSharp 构建的工业视觉图像显示控件库
Target Framework:.NET 8.0
目录
- 快速开始
- ImageDisplayControl — 核心控件
- Behaviors — 行为扩展
- ROI 类型详解
- 数据模型
- 转换器 (Converters)
- 接口 (Interfaces)
- 典型使用场景示例
- 8.1 场景一:基础图像浏览器(拖拽+缩放)
- 8.2 场景二:ROI 编辑工具
- 8.3 场景三:检测结果可视化
- 8.4 场景四:像素采样(颜色拾取)
- 8.5 场景五:完整工业视觉检测台
- 架构与扩展
- 9.1 业务项目扩展(算法叠加层)
- 常见问题
1. 快速开始
安装 / 引用
方式 A:项目引用(开发期)
<ItemGroup>
<ProjectReference Include="..\NjStdVision.Controls\NjStdVision.Controls.csproj" />
</ItemGroup>
方式 B:NuGet 包(推荐给其他项目)
dotnet add package NjStdVision.Controls --version 1.0.0
或手动添加:
<ItemGroup>
<PackageReference Include="NjStdVision.Controls" Version="1.0.0" />
</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 |
要显示的 SkiaSharp 图像 |
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) |
方法
| 方法 | 说明 |
|---|---|
FitSize() |
计算并应用适配变换,使图像居中显示且不超出控件边界 |
ScreenToImagePoint(Point) |
屏幕坐标 → 图像坐标 |
ImageToScreenPoint(Point) |
图像坐标 → 屏幕坐标 |
ImageRegionToScreenRect(Rect) |
图像区域 → 屏幕包围矩形 |
GetVisibleImageRect() |
当前视口在图像坐标系中的可见区域(用于裁剪) |
InvalidateOverlay() |
请求重绘标注层(ROI、自定义叠加) |
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();
注意事项
SKImage的生命周期由调用方管理,控件本身不会Dispose传入的图像。- 控件最小尺寸为 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%) |
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。采用 SKBitmap 缓存机制,性能优化。
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
PixelSampledCommand |
ICommand? |
null |
采样完成后执行的命令,参数为 PixelInfo |
TriggerKey |
KeyModifiers |
Alt |
触发采样所需的修饰键(None/Alt/Control/Shift) |
SampleInterval |
int |
16 |
采样节流间隔(毫秒),避免过于频繁的采样 |
工作流程
- 鼠标在控件上移动
- 检查是否按下了
TriggerKey修饰键 - 节流检查(距上次采样是否超过
SampleInterval毫秒) - 将屏幕坐标转换为图像坐标(使用变换矩阵逆变换)
- 从缓存的
SKBitmap中读取像素颜色 - 执行
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 与命中结果切换光标,并更新 ViewportStatus 供状态栏绑定。
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
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 — 像素信息
由 OptimizedImagePixelSamplerBehavior 采样后通过命令传递。
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; }
}
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 场景四:像素采样(颜色拾取)
<DockPanel>
<Border DockPanel.Dock="Bottom" Background="#222" Padding="8,4">
<StackPanel Orientation="Horizontal" Spacing="16">
<TextBlock Text="{Binding PixelCoord}" Foreground="White" />
<TextBlock Text="{Binding PixelRgb}" Foreground="White" />
<Border Width="20" Height="20"
CornerRadius="3"
Background="{Binding PixelHex}" />
</StackPanel>
</Border>
<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>
</DockPanel>
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">
<controls:ImageDisplayControl
Grid.Column="0"
SkiaImage="{Binding CurrentImage}"
IsFitSize="True">
<ix:Interaction.Behaviors>
<behaviors:ImageDragBehavior IsEnabled="{Binding IsViewMode}" />
<behaviors:ImageZoomBehavior />
<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}" />
<behaviors:OptimizedImagePixelSamplerBehavior
TriggerKey="Alt"
PixelSampledCommand="{Binding OnPixelSampledCommand}" />
</ix:Interaction.Behaviors>
</controls:ImageDisplayControl>
<StackPanel 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" />
<Separator />
<TextBlock Text="{Binding PixelInfo}" />
</StackPanel>
</Grid>
9. 架构与扩展
设计原则
ImageDisplayControl
├── RenderImageLayer ← SkiaImageDrawOp + 视口裁剪
├── RenderOverlayLayer ← IOverlayRenderable Behaviors
└── Behaviors
├── ImageDragBehavior ← 平移(Navigate 模式)
├── ImageZoomBehavior ← 缩放
├── ImageInteractionFeedbackBehavior ← 光标 + ViewportStatus
├── RoiCreationBehavior ← ROI 创建(含 Caliper)
├── RoiInteractionBehavior ← ROI 交互
├── RoiRenderBehavior ← ROI 只读渲染
├── DetectionResultBehavior
└── OptimizedImagePixelSamplerBehavior
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 |
CaliperParamConverter |
IOverlayRenderable |
MeasureEdgeOverlayBehavior |
RoiInteractionBehavior |
MeasurePreviewBehavior |
public class MyOverlayBehavior : Behavior<ImageDisplayControl>, IOverlayRenderable
{
public void Render(DrawingContext context, Matrix transform) { }
}
完整范例见 MeasureTestApp。
坐标系说明
| 坐标系 | 说明 | 用途 |
|---|---|---|
| 屏幕坐标 | 控件的 CSS 像素坐标,左上角为原点 | 鼠标事件原始坐标 |
| 图像坐标 | 图像像素坐标,左上角为原点 | ROI、检测结果数据存储 |
屏幕坐标 → 图像坐标:
var imagePoint = imageDisplayControl.ScreenToImagePoint(screenPoint);
图像坐标 → 屏幕坐标:
var screenPoint = imageDisplayControl.ImageToScreenPoint(imagePoint);
10. 常见问题
Q: 图像显示为空白 / 不显示
- 检查
SkiaImage是否为null - 检查
SKImage是否在显示前已被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: 像素采样不工作
- 确认按住了正确的修饰键(默认
Alt) - 确认图像已加载(
SkiaImage != null) - 检查
SampleInterval是否过大
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,Avalonia 会自动处理 DPI 缩放。确保 SKImage 的尺寸与原始图像像素尺寸一致,不要预先缩放。
文档版本:v1.1 | 更新日期:2026-06-19 | 对应包版本:NjStdVision.Controls 1.0.0
| 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.