79 Commits

Author SHA1 Message Date
LI Wei.lw e1c4c78510 已合并 PR 103: QFN检测模块
1.新增 ROI 对齐能力(RoiAlignmentProcessor + Alignment 基础类),打通模板位姿到 ROI 变换链路。
2.新增 QFN 一体检测算子(QfnAutoDetectionProcessor),串联模板匹配、ROI 对齐、中心/引脚空洞检测。
3.增强 QFN 相关 UI(新增 QFN 检测面板、ViewModel、图标、主界面入口与联动)。
4.改造 导航相机标定模块(新增采集服务接口/实现,标定控件重构,移除旧画布控件)。
5.优化部分 图像处理算子(如 Contrast/Threshold/GaussianBlur)与显示细节(线宽/线灰度自适应)。
2026-06-03 08:45:15 +08:00
ZHANG Zhengxuan 011854c42a 已合并 PR 98: CNC界面的调整和仿真执行功能
1、调整CNC树形结构,将位置为一级节点;文件名使用label控件显示;
2、CNC执行功能调试,新增运动,探测器,射线源的仿真,点击执行,判断到位,取图,计算的逻辑
3、将CNC执行结果的值类型, 写入mainfest.json

![image.png](http://cntao-ap-v83/HMQ-Solution/7ff128fd-5cc6-4feb-9529-2a03b2895662/_apis/git/repositories/e2c5485f-4369-4ed9-9fb9-d087ca4e04b6/pullRequests/98/attachments/image.png)
2026-06-02 09:07:30 +08:00
李伟 19b63fd419 模板匹配助手在保存模型时写入同名参考位姿 JSON。
训练 ROI 后记录基准中心和角度,保存模型时同步生成同名元数据文件,并在加载模型时自动回读,减少后续对齐配置的人工录入。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 17:08:20 +08:00
李伟 5b4ff89ef0 新增 QFN 一体检测算子,串联模板匹配与双路空洞检测。
将模板定位、中心 ROI 对齐、中心焊盘空洞和引脚空洞检测整合到单算子中,并输出统一判定结果,便于快速验证完整流程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 17:06:48 +08:00
李伟 1874c4a5bb 新增 ROI 对齐基础能力并打通到算子与 UI。
统一补齐对齐核心工具类、RoiAlignment 算子、模板匹配对齐扩展和多语言资源,便于在检测前稳定完成示教 ROI 到运行图的变换。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 17:04:32 +08:00
李伟 b0397365b2 屏蔽导航相机自动连接 2026-06-01 09:58:25 +08:00
SONG Tian a3a6bf7225 Merged PR 99: Updated ReconstructionNotifyService.cs 编译bug解决
Updated ReconstructionNotifyService.cs  编译bug解决
2026-06-01 09:28:56 +08:00
SONG Tian 580d61acae Updated ReconstructionNotifyService.cs 编译bug解决 2026-06-01 01:18:06 +00:00
李伟 703e548c31 启用导航相机自动连接 2026-05-27 09:23:10 +08:00
李伟 b3d39c3492 标定面板图像显示控件替换为PolygonRoiCanvas,删除旧ImageCanvasControl 2026-05-27 09:22:56 +08:00
李伟 bc626a0ca8 坐标标定工具改造:新增采集服务接口及实现,支持一键采集标定点 2026-05-27 09:22:44 +08:00
zhengxuan.zhang 84c1c5f16d CNC 检测模块输出的 数值类型如 bga检测和 孔隙检测的输出内容,要写入到 CNC 检测结果 manifest.json 合适的地址 2026-05-26 16:02:34 +08:00
李伟 030433cc92 优化:修复Logger类型错误,重写CLAHE算法,像素遍历改用unsafe指针加速 2026-05-26 13:28:52 +08:00
zhengxuan.zhang cdd0db95ff 调试CNC执行 2026-05-26 13:18:29 +08:00
李伟 82b7c32147 测量工具组线宽根据图像分辨率自适应 2026-05-26 11:35:25 +08:00
李伟 77f6a32eda 线灰度和辅助线粗细根据图像分辨率自适应,图像切换时自动重绘 2026-05-26 11:24:36 +08:00
李伟 7c06cd2def 新增QFN检测面板及按钮,统一三个模块ROI颜色为Cyan 2026-05-26 11:22:17 +08:00
李伟 0f24209e13 新增QFN引脚空洞率检测处理器及本地化资源 2026-05-26 11:22:04 +08:00
zhengxuan.zhang eb8d7a1491 优化当前加载的显示 2026-05-25 13:40:45 +08:00
zhengxuan.zhang d51d2b0013 修改CNC样式 √ 隐藏根节点,修改保存位置0 修改为位置1---N √ 运行对一级节点进行重命名 √ 2026-05-25 13:33:04 +08:00
ZHANG Zhengxuan 06c39c5ab4 已合并 PR 94: 界面调整及CNC完善
1、调整界面按钮:对流程图 连线样式的优化
2、修复CNC和普通模式的切换问题:当一种模式切换到另一种时,此时如果流程图或CNC编辑中有未保存的内容,要提醒保存,并根据用户的取消保存还是保存
3、修复CNC执行结果的缓存形式
4、探测器模拟一个接口能够返回图,验证XP集成层面能不能获取到图片;以及对相关链路加入日志
5、CNC位置节点新增数据源的手动输入和存图功能
6、高级CNC模块的插入逻辑,包括ROI的可视化再编辑
7、manifest.json文件 中文支持
2026-05-25 11:18:37 +08:00
zhengxuan.zhang 581ed2f3df 将流程图作为3级节点在左侧显示 ;优化布局宽度显示 ; 右侧详情面板的显示级别1级或2级 2026-05-25 10:59:39 +08:00
zhengxuan.zhang 03348a91ac Merge branch 'Develop/XP' into turbo-002-cnc
# Conflicts:
#	XP.Hardware.Detector/Implementations/SimulatedDetector.cs
2026-05-25 09:38:12 +08:00
SONG Tian 4afbadffd1 Merged PR 93: 控制软件socket功能开发
- XP.SCAN模块增加socket通讯,后期重构软件与控制软件在同一电脑进行socket通讯传递进行重构,后期可通过修改IP适配两台电脑的socket通讯重建。
2026-05-25 09:19:54 +08:00
TianSong cf15ed740f XP.SCAN模块增加socket通讯,后期重构软件与控制软件在同一电脑进行socket通讯传递进行重构,后期可通过修改IP适配两台电脑的socket通讯重建。 2026-05-25 08:26:49 +08:00
zhengxuan.zhang c443404bae 更新菜单按钮 2026-05-22 17:29:41 +08:00
zhengxuan.zhang 92213ffd0d Merge branch 'Develop/XP' into turbo-002-cnc
# Conflicts:
#	XplorePlane/Views/Main/MainWindow.xaml
2026-05-22 17:17:29 +08:00
zhengxuan.zhang 54d336a0b1 调整界面显示 2026-05-22 17:03:34 +08:00
QI Mingxuan 0b6554f139 解决添加虚拟探测器无法运行的问题:SimulatedDetector实现ApplyParametersInternalAsync。 2026-05-22 15:55:25 +08:00
QI Mingxuan 4943bc16b7 已合并 PR 90: 探测器直方图和优化合并至开发分支
1、重构探测器Hardware.Detector模块,统一设备调用接口,支持多探测器兼容,优化设备连接状态判断逻辑,新增校正帧数可配置功能。
2、优化Varex探测器校正流程,修复内存缓冲区对齐问题,增加指针、分辨率有效性校验,校正期间屏蔽帧回调、自动启停采集,规避SDK冲突与程序崩溃问题。
3、开发通用图像灰度直方图控件,优化资源释放逻辑。
2026-05-22 08:51:50 +08:00
LI Wei.lw 3777ad2d53 已合并 PR 86: 直线拟合、圆拟合、匹配等
新增功能:
1.边缘查找拟合直线工具
2.边缘查找拟合圆工具
3.海康相机接口集成
4.模板匹配助手窗口
5.ROI 对齐工具与 TM_Result 位姿扩展
6.白底/黑底缺陷检测
7.行灰度功能
优化与修复:
1.白底/黑底检测算法重构至 BackgroundDefectAnalyzer
2.黑底检测结果随清除测量一并清除
3.模板助手按钮布局与图标优化
4.导航相机日志改为英文
5.PixelConverter 支持 Bayer 格式解码
6.相机采集链断裂修复
2026-05-21 21:02:27 +08:00
zhengxuan.zhang de4a7121db 虚拟模式下 跳过联锁(Interlock)检查,因为虚拟模式没有真实安全门信 2026-05-21 17:15:27 +08:00
zhengxuan.zhang 43d0e7fa89 feat: 硬件虚拟化与CNC联动集成 - 运动控制/射线源模拟实现,CNC执行联动增强 2026-05-21 16:02:53 +08:00
zhengxuan.zhang 05c41a9a21 将CNC相关的事件合并,简化 2026-05-21 15:02:28 +08:00
zhengxuan.zhang 01b12bb246 修复JSON 序列化时的 Unicode 转义问题 2026-05-21 14:57:29 +08:00
zhengxuan.zhang 2ac84ecc85 修复高级算子的ROI编辑能力 2026-05-21 14:35:49 +08:00
QI Mingxuan 8905de6bab 更新ReleaseFiles。 2026-05-21 13:36:51 +08:00
QI Mingxuan 15e3e56856 已合并 PR 89: 探测器Hardware.Detector兼容多探测器的重构
探测器XP.Hardware.Detector类库为了更好集成新的探测器,统一接口方法,DetectorService重构为通过统一接口;
新增暗场校正和亮场校正帧数配置属性(默认 64,范围 1-128),config 加载校正帧数;
修正探测器IsConnected连接状态的判断逻辑。
2026-05-21 13:30:59 +08:00
QI Mingxuan 2d7cf17a3b 探测器XP.Hardware.Detector类库为了更好集成新的探测器,统一接口方法,DetectorService重构为通过统一接口;
新增暗场校正和亮场校正帧数配置属性(默认 64,范围 1-128),config 加载校正帧数;
修正探测器IsConnected连接状态的判断逻辑。
2026-05-21 13:19:30 +08:00
zhengxuan.zhang 2d14954bd3 高级模块插入后的再编辑问题,包括ROI的显示和调节,要支持实时调节 2026-05-21 11:17:10 +08:00
QI Mingxuan 119d03a02b VarexDetector:增益校正缓冲区改用 sizeof(uint) 分配(与 SDK DWORD 写入对齐),新增 _pOffsetBuffer 有效性校验和分辨率匹配检查;校正期间设置 _isCorrecting 标志跳过帧回调,防止缓冲区冲突;
VarexDetector:SetBinningMode/SetGainMode 变更后自动释放旧校正缓冲区;
DetectorService:暗场/亮场/坏像素校正及参数应用前自动停止采集,完成后恢复,避免 SDK 冲突;
DetectorConfigViewModel:校正流程中集成停止/恢复采集逻辑。
2026-05-21 10:41:55 +08:00
QI Mingxuan e6e776357d 优化直方图区域布局。 2026-05-21 10:40:38 +08:00
QI Mingxuan 5c56779f9f VarexDetectorConfig移除不支持的 3×3 Binning 选项,修正索引对齐。
新增多语言资源:暗场/亮场/坏像素校正确认对话框、进度提示、参数不一致提示。
2026-05-21 10:39:43 +08:00
QI Mingxuan d7c027b732 直方图将柱状图替换为面积图,优化密集数据显示效果,Y轴刻度自动取整支持 K/M 缩写,X 轴根据数据范围自动设置。 2026-05-21 10:37:28 +08:00
李伟 db0eac5d49 解决冲突 2026-05-20 15:41:04 +08:00
李伟 31825a43b9 合并 TURBO-615-RecognitionAndPositioning 到 ResolveConflicts,保留双方冲突内容 2026-05-20 15:32:43 +08:00
QI Mingxuan 2e4b2d714b 修改主程序解决方案生成到AnyCPU,增加外部库文件夹中dll和相关文件。 2026-05-20 15:22:09 +08:00
QI Mingxuan 375fb832f0 解决因Pull Request Merge Conflict Extension插件导致的中文乱码问题。 2026-05-20 15:00:21 +08:00
QI Mingxuan e3cfac5f09 Merged PR 82: 授权服务功能合并至开发分支
新增授权服务,XplorePlane 模块 ID 4、零件号 LS950-0071-5-1。
- 支持两种授权模式:CLMS 授权 (0) 和临时测试15分钟模式 (885);
- 支持通过接口查询授权信息。

配置项目如下:
```xml
<appSettings>
  <!-- 授权配置 | License configuration -->
  <add key="License:LicenseMode" value="0" />     <!-- 授权模式:0=CLMS 正式授权,885=临时测试模式 -->
  <add key="License:ModuleId" value="4" />        <!-- 模块 ID,XplorePlane 固定为 4 -->
  <add key="License:UseSma" value="false" />      <!-- 是否启用 SMA 检查 -->
  <add key="License:LicenseState" value="20" />   <!-- 上次授权状态:10=成功,20=失败(运行时由 LicenseService 自动写回)-->
</appSettings>
```

Readme文档详见 `XplorePlane\XP.Common\Documents\License.README.md`。
2026-05-20 10:11:10 +08:00
QI Mingxuan 14f41321c7 增加授权功能Readme文件。 2026-05-19 17:01:06 +08:00
QI Mingxuan 2d56f42d28 授权更新CLMS SDK(新版支持SMA),更新生成至anycpu。 2026-05-19 16:53:37 +08:00
zhengxuan.zhang 6abe391450 优化高级模块的CNC计算结果存储 2026-05-19 14:34:07 +08:00
zhengxuan.zhang 1546aec567 优化高级模块CNC执行的可视化
CNC执行 → PipelineExecutionService(返回 LastStepOutputData)
                  → CncExecutionService(调用 PushDetectionOverlay)
                  → MainViewportService(触发 DetectionOverlayUpdated 事件)
                  → ViewportPanelView(订阅事件,调用 DetectionOverlayRenderer)
                  → PolygonRoiCanvas.SetDetectionOverlayCanvas(插入叠加层 Canvas)
2026-05-19 14:10:16 +08:00
zhengxuan.zhang eb6ee48a5e CNC高级模块的运行后的可视化 2026-05-19 13:11:47 +08:00
zhengxuan.zhang 80c86e2ed7 孔隙检测模块引入到CNC 2026-05-19 11:38:31 +08:00
zhengxuan.zhang 3cfd115d72 高级模块的CNC插入功能 2026-05-19 11:21:28 +08:00
QI Mingxuan ef83a7637a 优化亮场校正和暗场校正的流程和功能,亮场校正后增加坏像素校正。 2026-05-18 17:30:22 +08:00
ZHANG Zhengxuan 3f14d14393 已合并 PR 78: UI调整、CNC执行与存储结果、新增设置页面
1、UI的更新,优化滤波器类型;流程图连线样式;算子编辑的属性;步长滑块的显示逻辑
2、新增设置页面,根据app.config 参数项设置页面;
3、优化CNC执行的可视化,新增位置节点的保存图像参数、手动输入图像;
![image.png](http://cntao-ap-v83/HMQ-Solution/7ff128fd-5cc6-4feb-9529-2a03b2895662/_apis/git/repositories/e2c5485f-4369-4ed9-9fb9-d087ca4e04b6/pullRequests/78/attachments/image.png)
4、参考viscom CNC存储逻辑,设计CNC存储结构
![image (2).png](http://cntao-ap-v83/HMQ-Solution/7ff128fd-5cc6-4feb-9529-2a03b2895662/_apis/git/repositories/e2c5485f-4369-4ed9-9fb9-d087ca4e04b6/pullRequests/78/attachments/image%20%282%29.png)
5、修复实时切换按钮
6、新增appstate的调试页面
2026-05-18 17:06:04 +08:00
zhengxuan.zhang 04da9cd798 对数据库DB文件进行忽略 2026-05-18 16:21:36 +08:00
zhengxuan.zhang d59550c492 删除螺旋扫描、语言设置合并到全局设置中; 2026-05-18 15:58:55 +08:00
李伟 e233f0fd96 feat: 新增边缘查找拟合圆工具 + 优化拟合交互
- 新增 EdgeCircleFitProcessor 算子(卡尺径向边缘检测 + Kasa/RANSAC圆拟合)
- 新增 EdgeCircleFitPanel 辅助面板(拖拽画圆交互)
- Ribbon快捷工具组新增「圆拟合」按钮
- 拟合后卡尺保持可编辑状态,支持调整后重新拟合
- 每次拟合自动清除上一次结果
- 拟合方法固定RANSAC,UI不暴露选择
- 结果标注简化:直线显示角度,圆显示半径和圆心坐标
- 不再显示内点/外点小圆点
- 添加中英文本地化资源
2026-05-18 15:03:34 +08:00
QI Mingxuan ed0fe92cbe 探测器设置界面增加图像灰度直方图,用于显示实时采集图像的灰度信息,优化图像灰度直方图的显示方式(无图像提示)和优化资源释放。 2026-05-18 14:41:05 +08:00
李伟 9c639f27cd 导航相机相关Log改为英文;添加一些图标 2026-05-18 13:55:24 +08:00
李伟 843c4d67a6 feat: 集成海康威视相机接口
- 新增 HikvisionCameraController 实现 ICameraController
- CameraFactory 支持 Basler/Hikvision 动态切换(config.json 配置)
- PixelConverter 支持 Bayer RG/GR/GB/BG 8-bit 解码
- 修复采集链断裂问题(finally 中触发下一帧)
- 相机设置面板:宽高和像素格式改为只读显示
- NavigationPropertyPanelViewModel 日志和状态文本改为英文
- 添加 MvCameraControl.Net.dll 到 ExternalLibraries
2026-05-18 13:11:26 +08:00
QI Mingxuan a9d56ebfbd 通用基础设施XP.Common新增 ImageHistogramControl 图像灰度直方图通用控件(使用SixLabors.ImageSharp 3.1.12),支持 Image<Rgba32> 和 byte[] 输入,支持多线程调用,Telerik RadChartView 渲染。 2026-05-18 09:17:39 +08:00
QI Mingxuan 346f4d9a9b XP.Common类库的控件Controls功能,按照功能/用途分子文件夹,移动和修改虚拟摇杆相关的文件。 2026-05-15 16:06:26 +08:00
QI Mingxuan 98d91efc19 更新RelesaeFiles,补全射线源Host. 2026-05-15 15:53:03 +08:00
QI Mingxuan 94f0649af8 XP.Common 类库中新增授权管理(License Management)功能模块,支持两种授权模式:CLMS 正式授权和临时测试模式。开发统一的授权服务接口,并在主项目中完成集成。 2026-05-15 15:50:35 +08:00
QI Mingxuan ad719d157b 统一 Dump、Logging、Database 等功能的配置文件加载和定义类,优化ConfigLoader,职责分散各个模块。 2026-05-15 15:49:02 +08:00
李伟 12938764b1 feat: 新增边缘查找拟合直线工具
- 新增 EdgeLineFitProcessor 算子(卡尺边缘检测 + 最小二乘/RANSAC直线拟合)
- 新增 EdgeLineFitPanel 辅助面板(参数配置、交互绘制卡尺)
- 支持任意角度旋转的卡尺区域,4个手柄控制长度/宽度
- 支持多次拟合累积显示,关闭面板后结果保留
- 极性箭头标识搜索方向(B→D / D→B / 双向)
- 卡尺亮绿色1px,拟合直线蓝色2px
- Ribbon快捷工具组新增「直线拟合」按钮
- 添加中英文本地化资源
2026-05-15 15:44:18 +08:00
李伟 7447463c1a feat: ROI 对齐工具与 TM_Result 位姿扩展
- Core: Pose2D、Point2D、RoiAlignment、AlignmentRecipe(示教多边形→运行图刚体变换)

- Processors: TemplateMatchAlignmentExtensions.ToPose2D / 四角与中心一致性校验

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 14:31:48 +08:00
李伟 9634e42396 ui: 模板助手按钮布局与 Segoe MDL2 图标
- 单张与参数:顶行 ROI/训练/加载/保存,底部仅运行匹配;批量测试按钮同步图标与 ToolTip

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 11:42:56 +08:00
李伟 e0eec42a2f feat: 模板匹配助手窗口与主视口 ROI 清除逻辑
- 新增模板助手/批量测试窗口、事件与 ViewModel,主窗口入口与 App 注册

- 运行匹配或批量测试前发布清除事件;视口通过 VisualTreeHelper 从父 Panel 移除持久虚线 ROI,避免 FindChild 失败时框残留

- TemplateMatchNative 等相关调整

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 10:42:20 +08:00
李伟 82465e6510 白底/黑底检测:轮廓与最远弦度量,UI 分色与标注优化
- 算子:输出轮廓顶点及顶点间最远弦(微米标定与原先一致)

- 视图:实线轮廓;白底红/黑底绿;尺寸文字置于 ROI 外右侧垂直居中

- 事件与 MainViewModel 载荷改为 BackgroundDefectDetectionItem

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 09:08:44 +08:00
李伟 baef619bd4 refactor: 白底/黑底检测算法迁至 BackgroundDefectAnalyzer
- 在 XP.ImageProcessing.Processors 新增静态分析类与 BackgroundDefectMode/BackgroundDefectBlob。

- MainViewModel 仅负责灰度 ROI 提取、坐标平移与 Prism 事件发布。
2026-05-14 16:27:16 +08:00
李伟 1ad33cc3e6 feat(viewport): 黑底检测与白/黑底结果随清除测量一并清除
- 新增黑底检测 Prism 事件与 MainViewModel 中 Otsu 二值化(Binary)流程,与白底(BinaryInv)对称。

- Viewport 统一 ROI 绘制与结果渲染;右键「清除所有测量」同时移除底色检测叠加层并复位 ROI 状态。
2026-05-14 16:11:14 +08:00
李伟 1fb789190c feat: 实现快捷工具栏白底检测功能 2026-05-14 15:54:15 +08:00
LI Wei.lw 5960f28bcf 已合并 PR 72: 行灰度功能添加
行灰度功能添加
2026-05-14 13:52:07 +08:00
李伟 7441526ed9 行灰度功能添加 2026-05-14 13:49:09 +08:00
246 changed files with 68192 additions and 1671 deletions
+2
View File
@@ -29,6 +29,7 @@ bld/
[Ll]ogs/
lib/
XP.ImageProcessing/
XP.ImageProcessing.SmokeTest/
ImageProcessing.sln
# 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构)
@@ -67,3 +68,4 @@ XplorePlane/data/
XplorePlane.Tests/bin_codex/
DataBase/XP.db
XplorePlane.Tests/TestResults/
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Config>
<Group ID="WriteCommon" DBNumber="31">
<Signal Name="SoftLive" Type="byte" StartAddr="0" IndexOrLength="" Remark="软件心跳,01周期变化表示PLC存活" />
<Signal Name="EmergencyStop" Type="byte" StartAddr="5" IndexOrLength="" Remark="急停,0:缺省,1:触发急停" />
<Signal Name="MC_SourceZ_Target" Type="single" StartAddr="10" IndexOrLength="" Remark="射线源Z目标位置" />
<Signal Name="MC_SourceZ_Speed" Type="single" StartAddr="14" IndexOrLength="" Remark="射线源Z运动速度" />
<Signal Name="MC_SourceZ_JogPos" Type="byte" StartAddr="18" IndexOrLength="" Remark="射线源Z正向Jog0:缺省,1:正向点动" />
<Signal Name="MC_SourceZ_JogNeg" Type="byte" StartAddr="19" IndexOrLength="" Remark="射线源Z反向Jog0:缺省,1:反向点动" />
<Signal Name="MC_SourceZ_Home" Type="byte" StartAddr="20" IndexOrLength="" Remark="射线源Z回零,0:缺省,1:触发回零" />
<Signal Name="MC_SourceZ_Stop" Type="byte" StartAddr="21" IndexOrLength="" Remark="射线源Z停止,0:缺省,1:触发停止" />
<Signal Name="MC_DetZ_Target" Type="single" StartAddr="22" IndexOrLength="" Remark="探测器Z目标位置" />
<Signal Name="MC_DetZ_Speed" Type="single" StartAddr="26" IndexOrLength="" Remark="探测器Z运动速度" />
<Signal Name="MC_DetZ_JogPos" Type="byte" StartAddr="30" IndexOrLength="" Remark="探测器Z正向Jog0:缺省,1:正向点动" />
<Signal Name="MC_DetZ_JogNeg" Type="byte" StartAddr="31" IndexOrLength="" Remark="探测器Z反向Jog0:缺省,1:反向点动" />
<Signal Name="MC_DetZ_Home" Type="byte" StartAddr="32" IndexOrLength="" Remark="探测器Z回零,0:缺省,1:触发回零" />
<Signal Name="MC_DetZ_Stop" Type="byte" StartAddr="33" IndexOrLength="" Remark="探测器Z停止,0:缺省,1:触发停止" />
<Signal Name="MC_StageX_Target" Type="single" StartAddr="34" IndexOrLength="" Remark="载物台X目标位置" />
<Signal Name="MC_StageX_Speed" Type="single" StartAddr="38" IndexOrLength="" Remark="载物台X运动速度" />
<Signal Name="MC_StageX_JogPos" Type="byte" StartAddr="42" IndexOrLength="" Remark="载物台X正向Jog0:缺省,1:正向点动" />
<Signal Name="MC_StageX_JogNeg" Type="byte" StartAddr="43" IndexOrLength="" Remark="载物台X反向Jog0:缺省,1:反向点动" />
<Signal Name="MC_StageX_Home" Type="byte" StartAddr="44" IndexOrLength="" Remark="载物台X回零,0:缺省,1:触发回零" />
<Signal Name="MC_StageX_Stop" Type="byte" StartAddr="45" IndexOrLength="" Remark="载物台X停止,0:缺省,1:触发停止" />
<Signal Name="MC_StageY_Target" Type="single" StartAddr="46" IndexOrLength="" Remark="载物台Y目标位置" />
<Signal Name="MC_StageY_Speed" Type="single" StartAddr="50" IndexOrLength="" Remark="载物台Y运动速度" />
<Signal Name="MC_StageY_JogPos" Type="byte" StartAddr="54" IndexOrLength="" Remark="载物台Y正向Jog0:缺省,1:正向点动" />
<Signal Name="MC_StageY_JogNeg" Type="byte" StartAddr="55" IndexOrLength="" Remark="载物台Y反向Jog0:缺省,1:反向点动" />
<Signal Name="MC_StageY_Home" Type="byte" StartAddr="56" IndexOrLength="" Remark="载物台Y回零,0:缺省,1:触发回零" />
<Signal Name="MC_StageY_Stop" Type="byte" StartAddr="57" IndexOrLength="" Remark="载物台Y停止,0:缺省,1:触发停止" />
<Signal Name="MC_DetSwing_Target" Type="single" StartAddr="58" IndexOrLength="" Remark="探测器摆动目标角度" />
<Signal Name="MC_DetSwing_Speed" Type="single" StartAddr="62" IndexOrLength="" Remark="探测器摆动运动速度" />
<Signal Name="MC_DetSwing_JogPos" Type="byte" StartAddr="66" IndexOrLength="" Remark="探测器摆动正向Jog,0:缺省,1:正向点动" />
<Signal Name="MC_DetSwing_JogNeg" Type="byte" StartAddr="67" IndexOrLength="" Remark="探测器摆动反向Jog,0:缺省,1:反向点动" />
<Signal Name="MC_DetSwing_Home" Type="byte" StartAddr="68" IndexOrLength="" Remark="探测器摆动回零,0:缺省,1:触发回零" />
<Signal Name="MC_DetSwing_Stop" Type="byte" StartAddr="69" IndexOrLength="" Remark="探测器摆动停止,0:缺省,1:触发停止" />
<Signal Name="MC_StageRot_Target" Type="single" StartAddr="70" IndexOrLength="" Remark="载物台旋转目标角度" />
<Signal Name="MC_StageRot_Speed" Type="single" StartAddr="74" IndexOrLength="" Remark="载物台旋转运动速度" />
<Signal Name="MC_StageRot_JogPos" Type="byte" StartAddr="78" IndexOrLength="" Remark="载物台旋转正向Jog,0:缺省,1:正向点动" />
<Signal Name="MC_StageRot_JogNeg" Type="byte" StartAddr="79" IndexOrLength="" Remark="载物台旋转反向Jog,0:缺省,1:反向点动" />
<Signal Name="MC_StageRot_Home" Type="byte" StartAddr="80" IndexOrLength="" Remark="载物台旋转回零,0:缺省,1:触发回零" />
<Signal Name="MC_StageRot_Stop" Type="byte" StartAddr="81" IndexOrLength="" Remark="载物台旋转停止,0:缺省,1:触发停止" />
<Signal Name="MC_FixRot_Target" Type="single" StartAddr="82" IndexOrLength="" Remark="夹具旋转目标角度" />
<Signal Name="MC_FixRot_Speed" Type="single" StartAddr="86" IndexOrLength="" Remark="夹具旋转运动速度" />
<Signal Name="MC_FixRot_JogPos" Type="byte" StartAddr="90" IndexOrLength="" Remark="夹具旋转正向Jog,0:缺省,1:正向点动" />
<Signal Name="MC_FixRot_JogNeg" Type="byte" StartAddr="91" IndexOrLength="" Remark="夹具旋转反向Jog,0:缺省,1:反向点动" />
<Signal Name="MC_FixRot_Home" Type="byte" StartAddr="92" IndexOrLength="" Remark="夹具旋转回零,0:缺省,1:触发回零" />
<Signal Name="MC_FixRot_Stop" Type="byte" StartAddr="93" IndexOrLength="" Remark="夹具旋转停止,0:缺省,1:触发停止" />
<Signal Name="MC_Door_Open" Type="byte" StartAddr="94" IndexOrLength="" Remark="安全门开门,0:缺省,1:触发开门" />
<Signal Name="MC_Door_Close" Type="byte" StartAddr="95" IndexOrLength="" Remark="安全门关门,0:缺省,1:触发关门" />
<Signal Name="MC_Door_Stop" Type="byte" StartAddr="96" IndexOrLength="" Remark="安全门停止,0:缺省,1:触发停止" />
<Signal Name="MC_VirtualJoystick_Enable" Type="bool" StartAddr="111" IndexOrLength="" Remark="虚拟摇杆使能"/>
<Signal Name="MC_SourceDetZ_Linkage_Enable" Type="bool" StartAddr="101" IndexOrLength="" Remark="联动使能"/>
</Group>
<Group ID="ReadCommon" DBNumber="31">
<Signal Name="ProbeA" Type="single" StartAddr="228" IndexOrLength="" Remark="测座角度A" />
<Signal Name="ProbeB" Type="string" StartAddr="225" IndexOrLength="20" Remark="测座角度B" />
<Signal Name="test" Type="byte" StartAddr="222" IndexOrLength="" Remark="" />
<Signal Name="PlcLive" Type="byte" StartAddr="200" IndexOrLength="" Remark="PLC心跳,01周期变化表示PLC存活" />
<Signal Name="PlcAlarm" Type="byte" StartAddr="201" IndexOrLength="" Remark="系统报警,0:缺省,10:有报警" />
<Signal Name="MC_SourceZ_Pos" Type="single" StartAddr="100" IndexOrLength="" Remark="射线源Z实际位置" />
<Signal Name="MC_DetZ_Pos" Type="single" StartAddr="104" IndexOrLength="" Remark="探测器Z实际位置" />
<Signal Name="MC_StageX_Pos" Type="single" StartAddr="108" IndexOrLength="" Remark="载物台X实际位置" />
<Signal Name="MC_StageY_Pos" Type="single" StartAddr="112" IndexOrLength="" Remark="载物台Y实际位置" />
<Signal Name="MC_DetSwing_Angle" Type="single" StartAddr="116" IndexOrLength="" Remark="探测器摆动实际角度" />
<Signal Name="MC_StageRot_Angle" Type="single" StartAddr="120" IndexOrLength="" Remark="载物台旋转实际角度" />
<Signal Name="MC_FixRot_Angle" Type="single" StartAddr="124" IndexOrLength="" Remark="夹具旋转实际角度" />
<Signal Name="MC_Door_Status" Type="byte" StartAddr="128" IndexOrLength="" Remark="安全门状态,0:未知,1:正在开门,2:已开,3:正在关门,4:已关,5:已锁定,6:故障" />
<Signal Name="MC_Door_Interlock" Type="byte" StartAddr="130" IndexOrLength="" Remark="安全门联锁信号,0:缺省(无联锁),10:联锁有效(禁止开门)" />
<Signal Name="MC_Joystick_Active" Type="bool" StartAddr="110" IndexOrLength="" Remark="实体摇杆输入"/>
</Group>
<Group ID="Status" DBNumber="4100">
<Signal Name="ScanMode" Type="byte" StartAddr="201" IndexOrLength="" Remark="扫描模式,0:缺省(空闲),具体值由PLC定义" />
</Group>
</Config>
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

@@ -0,0 +1,222 @@
{
"document": {
"pageSize": "A4",
"orientation": "Portrait",
"margins": { "top": 40, "bottom": 20, "left": 20, "right": 20 },
"header": {
"enabled": true,
"left": [
"${loc:Report_Title}",
"${loc:Report_Id}${metadata.reportId} | ${loc:Report_Date}${formatDate(metadata.inspectionDate)} | ${loc:Report_Sample}${metadata.sampleName}"
],
"rightImageKey": "companyLogo",
"fontSize": 7,
"color": "#666666",
"showLine": true
},
"footer": {
"enabled": true,
"left": ["${CompanyName}"],
"right": ["{currentPage} / {totalPages}"],
"fontSize": 8,
"color": "#666666",
"showLine": true
}
},
"pages": [
{
"type": "homepage",
"elements": [
{
"type": "row", "size": [170, 40], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "companyLogo", "size": [50, 30], "align": "left" },
{ "type": "text", "content": "${CompanyName}", "style": "companyName", "align": "left" }
]
},
{
"type": "column", "align": "right",
"children": [
{ "type": "image", "dataKey": "softwareLogo", "size": [15, 20], "align": "right" },
{ "type": "text", "content": "${SoftwareName}", "style": "companyName", "align": "right" }
]
}
]
},
{ "type": "spacer", "size": [170, 15], "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Title}", "style": "title", "positioning": "flow" },
{ "type": "spacer", "size": [170, 10], "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Id}${metadata.reportId}", "style": "homepageInfo", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Sample}${metadata.sampleName}", "style": "homepageInfo", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Description}${metadata.description}", "style": "homepageInfo", "positioning": "flow" },
{ "type": "image", "dataKey": "workpieceImage", "size": [160, 110], "border": true, "align": "center", "style": "imageDefault", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Operator}${metadata.operatorName}", "style": "homepageFooter", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Date}${formatDate(metadata.inspectionDate)}", "style": "homepageFooter", "positioning": "flow" }
]
},
{
"type": "summary",
"elements": [
{ "type": "text", "content": "${loc:Report_Summary}", "style": "homepageHeading", "positioning": "flow" },
{
"type": "table", "dataKey": "summaryTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
"columns": [
{ "header": "${loc:Field_InspectionType}", "field": "inspectionType", "width": 50, "align": "left" },
{ "header": "${loc:Field_Result}", "field": "classification", "width": 30, "align": "center", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } },
{ "header": "${loc:Field_Status}", "field": "status", "width": 30, "align": "center", "colorRules": { "合格": "#008000", "PASS": "#008000", "不合格": "#FF0000", "FAIL": "#FF0000" } }
]
}
]
},
{
"type": "metricData",
"elements": [
{ "type": "text", "content": "${loc:Page_MetricData}", "style": "heading", "positioning": "flow" },
{
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "lineMeasurementImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Measurement_Type}${measurementType}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Field_Point1}${point1}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Field_Point2}${point2}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_Distance}${actualDistance} ${unit}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_Angle}${angle}°", "style": "body", "align": "left" }
]
}
]
}
]
},
{
"type": "bgaInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_BgaInspection}", "style": "heading", "positioning": "flow" },
{
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "bgaInspectionImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Bga_Count}${bgaCount}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_FillRate}${formatPercent(fillRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Bga_TotalArea}${formatNumber(totalBgaArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Bga_TotalVoidArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Bga_VoidLimit}${formatPercent(voidLimit)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
},
{
"type": "table", "dataKey": "bgaBallsTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
"columns": [
{ "header": "${loc:Bga_BallIndex}", "field": "index", "width": 25, "align": "center" },
{ "header": "${loc:Table_VoidRate}", "field": "voidRate", "width": 40, "align": "center" },
{ "header": "${loc:Table_Area}", "field": "area", "width": 40, "align": "center" },
{ "header": "${loc:Table_Classification}", "field": "classification", "width": 35, "align": "center", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
},
{
"type": "voidInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_VoidInspection}", "style": "heading", "positioning": "flow" },
{
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "voidInspectionImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Void_RoiArea}${formatNumber(roiArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_TotalArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_Limit}${formatPercent(voidLimit)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_Count}${voidCount}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_MaxArea}${formatNumber(maxVoidArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
},
{
"type": "table", "dataKey": "voidsTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
"columns": [
{ "header": "${loc:Table_Index}", "field": "index", "width": 20, "align": "center" },
{ "header": "${loc:Table_Area}", "field": "area", "width": 35, "align": "center" },
{ "header": "${loc:Table_AreaPercent}", "field": "areaPercent", "width": 35, "align": "center" },
{ "header": "${loc:Table_CenterX}", "field": "centerX", "width": 30, "align": "center" },
{ "header": "${loc:Table_CenterY}", "field": "centerY", "width": 30, "align": "center" }
]
}
]
},
{
"type": "viaFillInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_ViaFillInspection}", "style": "heading", "positioning": "flow" },
{
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "viaFillImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Fill_Rate}${formatPercent(fillRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Fill_FullDistance}${formatNumber(fullDistance, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Fill_FillDistance}${formatNumber(fillDistance, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Fill_THTLimit}${formatPercent(thtLimit)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
}
]
}
],
"styles": {
"title": { "font": "auto", "size": 28, "bold": true, "italic": false, "color": "#1a1a1a", "align": "center" },
"companyName": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center" },
"homepageInfo": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "left", "paddingLeft": 10 },
"homepageFooter": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "center" },
"homepageHeading": { "font": "auto", "size": 14, "bold": true, "italic": false, "color": "#333333", "align": "left" },
"heading": { "font": "auto", "size": 16, "bold": true, "italic": false, "color": "#333333", "align": "left", "marginBottom": 3 },
"body": { "font": "auto", "size": 11, "bold": false, "italic": false, "color": "#333333", "align": "left" },
"bodyBold": { "font": "auto", "size": 11, "bold": true, "italic": false, "color": "#1a1a1a", "align": "left" },
"conclusion": { "font": "auto", "size": 12, "bold": true, "italic": false, "color": "#333333", "align": "left", "marginTop": 3 },
"imageDefault": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "center", "marginTop": 5, "marginBottom": 5 },
"tableHeader": { "font": "auto", "size": 10, "bold": true, "italic": false, "color": "#ffffff", "align": "center", "backgroundColor": "#4472C4" },
"tableDefault": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center", "marginTop": 5 },
"tableCell": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center" }
}
}
Binary file not shown.
@@ -0,0 +1,4 @@
// Flag, ob die Trace-Meldungen asynchron zu protokolliern sind: 1 für asynchron, 0 für synchron.
1
// Kategorie Konfigurationen: <Kategorie-Name> <Enabled[0/1]>
FXDriver 0
+2 -1
View File
@@ -1,4 +1,5 @@
{
"Language": "zh-CN",
"LogLevel": "Debug"
"LogLevel": "Debug",
"CameraType": "Hikvision"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
+2
View File
@@ -108,3 +108,5 @@ dotnet build XplorePlane.sln -c Release
- [x] 主界面硬件栏相机设置按钮
- [x] 打通与硬件层的调用流程
- [x] 打通与图像层的调用流程
- [ ] CNC的执行、存储逻辑的开发测试
- [ ] 涉及到图像校准,矩阵
+11 -4
View File
@@ -11,6 +11,11 @@
<add key="UserManual" value="UserManual.pdf" />
<add key="DeviceId" value="PlanerCT001" />
<!-- 授权配置 | License configuration -->
<add key="License:LicenseMode" value="0" />
<add key="License:ModuleId" value="4" />
<add key="License:UseSma" value="false" />
<add key="License:LicenseState" value="20" />
<!-- Serilog日志配置 -->
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
@@ -66,7 +71,6 @@
<add key="Detector:Port" value="5000" />
<add key="Detector:SavePath" value="D:\XplorePlane\DetectorImages" />
<add key="Detector:AutoSave" value="true" />
<!-- Varex 探测器专属配置 | Varex Detector Specific Configuration -->
<!-- Binning 模式: Bin1x1, Bin2x2, Bin4x4 | Binning mode: Bin1x1, Bin2x2, Bin4x4 -->
<add key="Detector:Varex:BinningMode" value="Bin1x1" />
<!-- 增益模式: Low, High | Gain mode: Low, High -->
@@ -78,14 +82,13 @@
<add key="Detector:Varex:ROI_Y" value="0" />
<add key="Detector:Varex:ROI_Width" value="2880" />
<add key="Detector:Varex:ROI_Height" value="2880" />
<!-- iRay 探测器专属配置 | iRay Detector Specific Configuration -->
<!-- 采集模式: Continuous, SingleFrame | Acquisition mode: Continuous, SingleFrame -->
<add key="Detector:IRay:AcquisitionMode" value="Continuous" />
<!-- 默认增益值 | Default gain value -->
<add key="Detector:IRay:DefaultGain" value="1.0" />
<!-- 校正配置 | Correction Configuration -->
<add key="Detector:Correction:DarkFrameCount" value="10" />
<add key="Detector:Correction:GainFrameCount" value="10" />
<add key="Detector:Correction:DarkFrameCount" value="64" />
<add key="Detector:Correction:GainFrameCount" value="64" />
<add key="Detector:Correction:SaveCorrectionData" value="true" />
<!-- 操作超时配置 | Operation Timeout Configuration -->
<add key="Detector:InitializationTimeout" value="30000" />
@@ -163,6 +166,10 @@
<!-- 运行参数 | Runtime parameters -->
<add key="MotionControl:PollingInterval" value="500" />
<add key="MotionControl:DefaultVelocity" value="500" />
<!-- 射线源与探测器Z轴联动配置 | Source-Detector Z-axis linkage configuration -->
<add key="MotionControl:SourceDetectorZLinkage:Enabled" value="true" />
<add key="MotionControl:SourceDetectorZLinkage:TriggerThreshold" value="1.0" />
<add key="MotionControl:SourceDetectorZLinkage:SpeedPercent" value="100" />
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
View File
@@ -1018,6 +1018,14 @@
}
}
},
"SixLabors.ImageSharp/3.1.12": {
"runtime": {
"lib/net6.0/SixLabors.ImageSharp.dll": {
"assemblyVersion": "3.0.0.0",
"fileVersion": "3.1.12.0"
}
}
},
"SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
"dependencies": {
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
@@ -2256,6 +2264,7 @@
"Serilog.Settings.Configuration": "10.0.0",
"Serilog.Sinks.Console": "6.1.1",
"Serilog.Sinks.File": "7.0.0",
"SixLabors.ImageSharp": "3.1.12",
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
},
"runtime": {
@@ -2422,6 +2431,7 @@
"Serilog": "4.3.1",
"Serilog.Sinks.Console": "6.1.1",
"Serilog.Sinks.File": "7.0.0",
"XP.Common": "1.0.0",
"XP.ImageProcessing.Core": "1.0.0"
},
"runtime": {
@@ -3311,6 +3321,13 @@
"path": "sharpdx.mathematics/4.2.0",
"hashPath": "sharpdx.mathematics.4.2.0.nupkg.sha512"
},
"SixLabors.ImageSharp/3.1.12": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==",
"path": "sixlabors.imagesharp/3.1.12",
"hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512"
},
"SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
"type": "package",
"serviceable": true,
Binary file not shown.
+9 -62
View File
@@ -5,127 +5,88 @@
</sectionGroup>
</configSections>
<appSettings>
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
<add key="Language" value="ZhCN" />
<add key="XpData:RootPath" value="D:\XPData" />
<add key="UserManual" value="UserManual.pdf" />
<add key="DeviceId" value="PlanerCT001" />
<!-- Serilog日志配置 -->
<add key="License:LicenseMode" value="0" />
<add key="License:ModuleId" value="4" />
<add key="License:UseSma" value="false" />
<add key="License:LicenseState" value="10" />
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
<add key="Serilog:MinimumLevel" value="Debug" />
<add key="Serilog:EnableConsole" value="true" />
<add key="Serilog:RollingInterval" value="Day" />
<add key="Serilog:FileSizeLimitMB" value="100" />
<add key="Serilog:RetainedFileCountLimit" value="365" />
<!-- 数据库SQLite配置 -->
<add key="Sqlite:DbFilePath" value="D:\XplorePlane\DataBase\XP.db" />
<add key="Sqlite:ConnectionTimeout" value="10" />
<add key="Sqlite:CreateIfNotExists" value="true" />
<!-- 是否启用SQLite WAL模式(提升并发读写性能,默认true) -->
<add key="Sqlite:EnableWalMode" value="true" />
<!-- 是否开启SQL操作日志(记录所有执行的SQL语句,默认false -->
<add key="Sqlite:EnableSqlLogging" value="false" />
<!-- 射线源配置 -->
<!-- 射线源类型 | Ray Source Type -->
<!-- 可选值: Comet225 | Available values: Comet225 -->
<add key="RaySource:SourceType" value="Comet225" />
<add key="RaySource:SerialNumber" value="SN08602861" />
<add key="RaySource:TotalLifeThreshold" value="10" />
<!-- PVI通讯参数 | PVI Communication Parameters -->
<add key="RaySource:PlcIpAddress" value="192.168.12.10" />
<add key="RaySource:PlcPort" value="11159" />
<add key="RaySource:PortNumber" value="11" />
<add key="RaySource:StationNumber" value="1" />
<add key="RaySource:CpuName" value="cpu" />
<add key="RaySource:ConnectionTimeout" value="30000" />
<!-- 硬件参数范围 | Hardware Parameter Ranges -->
<add key="RaySource:MinVoltage" value="20" />
<add key="RaySource:MaxVoltage" value="225" />
<add key="RaySource:MinCurrent" value="10" />
<add key="RaySource:MaxCurrent" value="1440" />
<!-- 外部程序配置 | External Program Configuration -->
<add key="RaySource:AdvanceExePath" value="C:\Program Files (x86)\Feinfocus\FXEControl_3.1.1.65\FXEControl.exe" />
<!-- 操作超时配置 | Operation Timeout Configuration -->
<add key="RaySource:RaySource:InitializationTimeout" value="30000" />
<add key="RaySource:WarmUpTimeout" value="600000" />
<add key="RaySource:StartUpTimeout" value="1800000" />
<add key="RaySource:AutoCenterTimeout" value="1200000" />
<add key="RaySource:FilamentAdjustTimeout" value="1200000" />
<add key="RaySource:GeneralOperationTimeout" value="100000" />
<!-- 探测器配置 -->
<!-- 探测器类型 | Detector Type -->
<!-- 可选值: Varex, IRay, Hamamatsu | Available values: Varex, IRay, Hamamatsu -->
<add key="Detector:Type" value="Varex" />
<!-- 通用配置 | Common Configuration -->
<add key="Detector:IP" value="192.168.1.200" />
<add key="Detector:Port" value="5000" />
<add key="Detector:SavePath" value="D:\XplorePlane\DetectorImages" />
<add key="Detector:AutoSave" value="true" />
<!-- Varex 探测器专属配置 | Varex Detector Specific Configuration -->
<!-- Binning 模式: Bin1x1, Bin2x2, Bin4x4 | Binning mode: Bin1x1, Bin2x2, Bin4x4 -->
<add key="Detector:Varex:BinningMode" value="Bin1x1" />
<!-- 增益模式: Low, High | Gain mode: Low, High -->
<add key="Detector:Varex:GainMode" value="High" />
<!-- 曝光时间(毫秒)| Exposure time (milliseconds) -->
<add key="Detector:Varex:ExposureTime" value="100" />
<!-- ROI 区域 | ROI Region -->
<add key="Detector:Varex:ROI_X" value="0" />
<add key="Detector:Varex:ROI_Y" value="0" />
<add key="Detector:Varex:ROI_Width" value="2880" />
<add key="Detector:Varex:ROI_Height" value="2880" />
<!-- iRay 探测器专属配置 | iRay Detector Specific Configuration -->
<!-- 采集模式: Continuous, SingleFrame | Acquisition mode: Continuous, SingleFrame -->
<add key="Detector:IRay:AcquisitionMode" value="Continuous" />
<!-- 默认增益值 | Default gain value -->
<add key="Detector:IRay:DefaultGain" value="1.0" />
<!-- 校正配置 | Correction Configuration -->
<add key="Detector:Correction:DarkFrameCount" value="10" />
<add key="Detector:Correction:GainFrameCount" value="10" />
<add key="Detector:Correction:SaveCorrectionData" value="true" />
<!-- 操作超时配置 | Operation Timeout Configuration -->
<add key="Detector:InitializationTimeout" value="30000" />
<add key="Detector:AcquisitionTimeout" value="10000" />
<add key="Detector:CorrectionTimeout" value="60000" />
<!-- 主界面实时图像与探测器帧流水线 -->
<add key="MainViewport:RealtimeEnabledDefault" value="true" />
<add key="DetectorPipeline:AcquireQueueCapacity" value="16" />
<add key="DetectorPipeline:ProcessQueueCapacity" value="8" />
<add key="DetectorPipeline:ProcessEveryNFrames" value="1" />
<!-- Dump 配置 | Dump Configuration -->
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
<add key="Dump:EnableScheduledDump" value="false" />
<add key="Dump:ScheduledIntervalMinutes" value="60" />
<add key="Dump:MiniDumpSizeLimitMB" value="100" />
<add key="Dump:RetentionDays" value="7" />
<!-- PLC 配置 | PLC Configuration -->
<add key="Plc:IpAddress" value="192.168.0.1" />
<add key="Plc:Port" value="102" />
<add key="Plc:Rack" value="0" />
<add key="Plc:Slot" value="1" />
<!-- PlcType可选值: S200Smart, S300, S400, S1200, S1500 | PlcType Available values: S200Smart, S300, S400, S1200, S1500 -->
<add key="Plc:PlcType" value="S1200" />
<!-- 数据块配置 | Data Block Configuration -->
<add key="Plc:ReadDbBlock" value="DB31" />
<add key="Plc:WriteDbBlock" value="DB31" />
<add key="Plc:ReadStartAddress" value="0" />
<add key="Plc:ReadLength" value="200" />
<!-- 批量读取周期(毫秒)| Bulk read interval (ms) -->
<add key="Plc:BulkReadIntervalMs" value="250" />
<!-- 超时配置 | Timeout Configuration -->
<add key="Plc:ConnectTimeoutMs" value="3000" />
<add key="Plc:ReadTimeoutMs" value="1000" />
<add key="Plc:WriteTimeoutMs" value="1000" />
<!-- 自动重连 | Auto Reconnection -->
<add key="Plc:bReConnect" value="true" />
<!-- 直线轴配置(单位:mm| Linear axis config (unit: mm) -->
<add key="MotionControl:SourceZ:Min" value="-500" />
<add key="MotionControl:SourceZ:Max" value="500" />
<add key="MotionControl:SourceZ:Origin" value="0" />
@@ -138,7 +99,6 @@
<add key="MotionControl:StageY:Min" value="-150" />
<add key="MotionControl:StageY:Max" value="150" />
<add key="MotionControl:StageY:Origin" value="0" />
<!-- 旋转轴配置(单位:度)| Rotary axis config (unit: degrees) -->
<add key="MotionControl:DetectorSwing:Min" value="-45" />
<add key="MotionControl:DetectorSwing:Max" value="45" />
<add key="MotionControl:DetectorSwing:Origin" value="0" />
@@ -151,46 +111,33 @@
<add key="MotionControl:FixtureRotation:Max" value="90" />
<add key="MotionControl:FixtureRotation:Origin" value="0" />
<add key="MotionControl:FixtureRotation:Enabled" value="false" />
<!-- 几何原点(mm| Geometry origins (mm) -->
<add key="MotionControl:Geometry:SourceZOrigin" value="0" />
<add key="MotionControl:Geometry:DetectorZOrigin" value="600" />
<add key="MotionControl:Geometry:StageRotationCenterZ" value="300" />
<!-- 探测器摆动几何参数(mm| Detector swing geometry parameters (mm) -->
<!-- SwingPivotOffset: Pivot 相对于 DetectorZ 绝对坐标的 Z 方向偏移,正值表示 Pivot 在 DetectorZ_abs 上方 -->
<add key="MotionControl:Geometry:SwingPivotOffset" value="80" />
<!-- SwingRadius: Pivot 到探测器感光面中心的距离,为 0 时退化为无摆动模型 -->
<add key="MotionControl:Geometry:SwingRadius" value="200" />
<!-- 运行参数 | Runtime parameters -->
<add key="MotionControl:PollingInterval" value="500" />
<add key="MotionControl:DefaultVelocity" value="500" />
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
<add key="MotionControl:SourceDetectorZLinkage:Enabled" value="true" />
<add key="MotionControl:SourceDetectorZLinkage:TriggerThreshold"
value="1.0" />
<add key="MotionControl:SourceDetectorZLinkage:SpeedPercent"
value="100" />
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
<!-- 输出文件名模式 | File name pattern -->
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
<!-- 文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates-->
<add key="Report:AutoIncrementOnDuplicate" value="true" />
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
<add key="Report:AutoOpenAfterGenerate" value="false" />
<!-- 默认页面尺寸 | Default page size (A4) -->
<add key="Report:DefaultPageSize" value="A4" />
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
<add key="Report:DefaultOrientation" value="Portrait" />
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
<add key="Report:MarginTop" value="20" />
<add key="Report:MarginBottom" value="20" />
<add key="Report:MarginLeft" value="20" />
<add key="Report:MarginRight" value="20" />
<!-- 报告中显示的公司名称 | Company name displayed in report -->
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
<add key="Report:CompanyLogo" value="Templates\Logo.png" />
<!-- 软件信息配置 | Software Information Configuration -->
<add key="Report:SoftwareName" value="XplorePlane" />
<add key="Report:SoftwareLogo" value="Templates\Logo2.png" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cal="clr-namespace:XP.Camera.Calibration"
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
mc:Ignorable="d"
d:DesignHeight="850"
d:DesignWidth="1400">
@@ -88,6 +89,20 @@
<Image Source="/XP.Camera;component/Calibration/Resources/外部导入.png" Width="24" Height="24" />
</Button.Tag>
</Button>
<Button Content="采集当前点"
Command="{Binding CapturePointCommand}" FontFamily="Segoe UI"
Style="{StaticResource ToolbarButtonStyle}">
<Button.Tag>
<Image Source="/XP.Camera;component/Calibration/Resources/执行.png" Width="24" Height="24" />
</Button.Tag>
</Button>
<Button Content="删除选中"
Command="{Binding DeleteSelectedPointCommand}" FontFamily="Segoe UI"
Style="{StaticResource ToolbarButtonStyle}">
<Button.Tag>
<TextBlock Text="✕" FontSize="20" Foreground="Red" HorizontalAlignment="Center" />
</Button.Tag>
</Button>
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=CalibrationExecute}"
Command="{Binding CalibrateCommand}" FontFamily="Segoe UI"
Style="{StaticResource ToolbarButtonStyle}">
@@ -113,6 +128,32 @@
VerticalAlignment="Center" FontFamily="Segoe UI"
IsChecked="{Binding ShowWorldCoordinates}"
Margin="10,0,0,0" FontSize="13" Foreground="{StaticResource TextColor}" />
<ToggleButton IsChecked="{Binding IsLiveView}" VerticalAlignment="Center" Margin="10,0,0,0"
Width="80" Height="66" Cursor="Hand" FontFamily="Segoe UI">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="Bd" Background="White" BorderBrush="#E1E1E1"
BorderThickness="1" CornerRadius="6">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock x:Name="Icon" Text="▶" FontSize="20" HorizontalAlignment="Center" Margin="0,2,0,3" />
<TextBlock x:Name="Label" Text="实时" FontSize="10.5" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Icon" Property="Text" Value="⏸" />
<Setter TargetName="Label" Property="Text" Value="冻结" />
<Setter TargetName="Bd" Property="Background" Value="#E8F5E9" />
<Setter TargetName="Bd" Property="BorderBrush" Value="#81C784" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#EAF2FB" />
<Setter TargetName="Bd" Property="BorderBrush" Value="#B0D4F1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</StackPanel>
</Border>
@@ -136,6 +177,7 @@
<DataGrid Grid.Row="1" AutoGenerateColumns="False" CanUserAddRows="True"
ItemsSource="{Binding CalibrationPoints}"
SelectedItem="{Binding SelectedPoint}"
HeadersVisibility="Column" GridLinesVisibility="All"
FontFamily="Segoe UI"
BorderBrush="{StaticResource BorderColor}" BorderThickness="1">
@@ -159,7 +201,8 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,12,8" />
<roi:PolygonRoiCanvas Grid.Row="0" x:Name="roiCanvas" Margin="12,12,12,8"
ImageSource="{Binding ImageSource}" />
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
Margin="12,0,12,12" Padding="12" MinHeight="80">
@@ -1,10 +1,9 @@
using System.Drawing;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using XP.Camera.Calibration.ViewModels;
using WpfBrushes = System.Windows.Media.Brushes;
using WpfColor = System.Windows.Media.Color;
using XP.ImageProcessing.RoiControl.Controls;
namespace XP.Camera.Calibration.Controls;
@@ -23,82 +22,42 @@ public partial class CalibrationControl : UserControl
if (DataContext is CalibrationViewModel viewModel)
{
_viewModel = viewModel;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
}
}
_viewModel.ImageLoadedRequested += (s, e) =>
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
imageCanvas.ReferenceImage = _viewModel.ImageSource;
imageCanvas.RoiCanvas.Children.Clear();
if (e.PropertyName == nameof(CalibrationViewModel.OverlayImage))
{
UpdateDetectionOverlay();
}
}
private void UpdateDetectionOverlay()
{
if (_viewModel?.OverlayImage == null)
{
roiCanvas.ClearDetectionOverlay();
return;
}
var overlayCanvas = new Canvas
{
Width = _viewModel.OverlayImage.PixelWidth,
Height = _viewModel.OverlayImage.PixelHeight,
IsHitTestVisible = false
};
imageCanvas.CanvasRightMouseUp += ImageCanvas_RightMouseUp;
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
}
}
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
var image = new System.Windows.Controls.Image
{
if (_viewModel?.CurrentImage == null) return;
double zoom = e.Delta > 0 ? 1.1 : 0.9;
imageCanvas.ZoomScale *= zoom;
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
}
private void ImageCanvas_RightMouseUp(object? sender, MouseButtonEventArgs e)
{
if (_viewModel?.CurrentImage == null) return;
var pos = e.GetPosition(imageCanvas.RoiCanvas);
float imageX = (float)pos.X;
float imageY = (float)pos.Y;
if (imageX >= 0 && imageX < _viewModel.CurrentImage.Width &&
imageY >= 0 && imageY < _viewModel.CurrentImage.Height)
{
var pixelPoint = new PointF(imageX, imageY);
var worldPoint = _viewModel.ConvertPixelToWorld(pixelPoint);
_viewModel.StatusText = $"像素坐标: ({imageX:F2}, {imageY:F2})\n世界坐标: ({worldPoint.X:F2}, {worldPoint.Y:F2})";
DrawMarkerOnCanvas(imageX, imageY, worldPoint);
}
}
private void DrawMarkerOnCanvas(float imageX, float imageY, PointF worldPoint)
{
imageCanvas.RoiCanvas.Children.Clear();
var ellipse = new System.Windows.Shapes.Ellipse
{
Width = 10, Height = 10,
Stroke = WpfBrushes.Red, StrokeThickness = 2,
Fill = WpfBrushes.Transparent
Source = _viewModel.OverlayImage,
Width = _viewModel.OverlayImage.PixelWidth,
Height = _viewModel.OverlayImage.PixelHeight,
IsHitTestVisible = false
};
Canvas.SetLeft(ellipse, imageX - 5);
Canvas.SetTop(ellipse, imageY - 5);
imageCanvas.RoiCanvas.Children.Add(ellipse);
var pixelText = new TextBlock
{
Text = $"P:({imageX:F0},{imageY:F0})",
Foreground = WpfBrushes.Red, FontSize = 12,
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
};
Canvas.SetLeft(pixelText, imageX + 10);
Canvas.SetTop(pixelText, imageY - 20);
imageCanvas.RoiCanvas.Children.Add(pixelText);
if (_viewModel?.ShowWorldCoordinates == true)
{
var worldText = new TextBlock
{
Text = $"W:({worldPoint.X:F2},{worldPoint.Y:F2})",
Foreground = WpfBrushes.Blue, FontSize = 12,
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
};
Canvas.SetLeft(worldText, imageX + 10);
Canvas.SetTop(worldText, imageY + 5);
imageCanvas.RoiCanvas.Children.Add(worldText);
}
overlayCanvas.Children.Add(image);
roiCanvas.SetDetectionOverlayCanvas(overlayCanvas);
}
}
@@ -5,6 +5,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cal="clr-namespace:XP.Camera.Calibration"
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
mc:Ignorable="d"
d:DesignHeight="900"
d:DesignWidth="1600">
@@ -168,7 +169,8 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,8,8" />
<roi:PolygonRoiCanvas Grid.Row="0" x:Name="roiCanvas" Margin="12,12,8,8"
ImageSource="{Binding ImageSource}" />
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
Margin="12,0,8,12" Padding="12" Height="70">
<Grid>
@@ -1,46 +1,13 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using XP.Camera.Calibration.ViewModels;
namespace XP.Camera.Calibration.Controls;
public partial class ChessboardCalibrationControl : UserControl
{
private ChessboardCalibrationViewModel? _viewModel;
public ChessboardCalibrationControl()
{
InitializeComponent();
Loaded += ChessboardCalibrationControl_Loaded;
}
private void ChessboardCalibrationControl_Loaded(object sender, RoutedEventArgs e)
{
if (DataContext is ChessboardCalibrationViewModel viewModel)
{
_viewModel = viewModel;
_viewModel.ImageLoadedRequested += (s, e) =>
{
imageCanvas.ReferenceImage = _viewModel.ImageSource;
};
_viewModel.ImageClearedRequested += (s, e) =>
{
imageCanvas.ReferenceImage = null;
};
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
}
}
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
{
if (_viewModel?.ImageSource == null) return;
double zoom = e.Delta > 0 ? 1.1 : 0.9;
imageCanvas.ZoomScale *= zoom;
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
}
}
@@ -1,33 +0,0 @@
<UserControl x:Class="XP.Camera.Calibration.Controls.ImageCanvasControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800" x:Name="imageCanvasControl">
<Border ClipToBounds="True" RenderOptions.BitmapScalingMode="NearestNeighbor">
<Viewbox>
<AdornerDecorator x:Name="adorner" MouseWheel="Adorner_MouseWheel">
<AdornerDecorator.RenderTransform>
<TransformGroup>
<TranslateTransform X="{Binding PanningOffsetX, ElementName=imageCanvasControl}"
Y="{Binding PanningOffsetY, ElementName=imageCanvasControl}" />
<ScaleTransform ScaleX="{Binding ZoomScale, ElementName=imageCanvasControl}"
ScaleY="{Binding ZoomScale, ElementName=imageCanvasControl}"
CenterX="{Binding ZoomCenter.X, ElementName=imageCanvasControl}"
CenterY="{Binding ZoomCenter.Y, ElementName=imageCanvasControl}" />
</TransformGroup>
</AdornerDecorator.RenderTransform>
<Grid PreviewMouseMove="Canvas_MouseMove"
PreviewMouseLeftButtonUp="Canvas_MouseLeftButtonUp"
PreviewMouseRightButtonUp="Canvas_MouseRightButtonUp"
MouseEnter="Canvas_MouseEnter"
PreviewMouseLeftButtonDown="Canvas_MouseLeftButtonDown"
PreviewMouseRightButtonDown="Canvas_MouseRightButtonDown">
<ContentPresenter Content="{Binding RoiCanvas, ElementName=imageCanvasControl}"
SizeChanged="ContentPresenter_SizeChanged" />
</Grid>
</AdornerDecorator>
</Viewbox>
</Border>
</UserControl>
@@ -1,229 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace XP.Camera.Calibration.Controls;
/// <summary>
/// 图像画布控件 - 提供图像显示、缩放、平移功能
/// </summary>
public partial class ImageCanvasControl : UserControl
{
private Point mouseDownPoint = new Point();
#region Dependency Properties
public static readonly DependencyProperty ZoomScaleProperty =
DependencyProperty.Register("ZoomScale", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
public static readonly DependencyProperty ZoomCenterProperty =
DependencyProperty.Register("ZoomCenter", typeof(Point), typeof(ImageCanvasControl), new PropertyMetadata(new Point()));
public static readonly DependencyProperty PanningOffsetXProperty =
DependencyProperty.Register("PanningOffsetX", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
public static readonly DependencyProperty PanningOffsetYProperty =
DependencyProperty.Register("PanningOffsetY", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
public static readonly DependencyProperty ReferenceImageProperty =
DependencyProperty.Register("ReferenceImage", typeof(BitmapSource), typeof(ImageCanvasControl),
new UIPropertyMetadata(null, ReferenceImageChanged));
public static readonly DependencyProperty ImageScaleFactorProperty =
DependencyProperty.Register("ImageScaleFactor", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
public static readonly DependencyProperty MaxImageWidthProperty =
DependencyProperty.Register("MaxImageWidth", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
public static readonly DependencyProperty MaxImageHeightProperty =
DependencyProperty.Register("MaxImageHeight", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
public static readonly DependencyProperty EnablePanningProperty =
DependencyProperty.Register("EnablePanning", typeof(bool), typeof(ImageCanvasControl), new PropertyMetadata(true));
#endregion
#region Properties
public double ZoomScale
{
get => (double)GetValue(ZoomScaleProperty);
set => SetValue(ZoomScaleProperty, value);
}
public Point ZoomCenter
{
get => (Point)GetValue(ZoomCenterProperty);
set => SetValue(ZoomCenterProperty, value);
}
public double PanningOffsetX
{
get => (double)GetValue(PanningOffsetXProperty);
set => SetValue(PanningOffsetXProperty, value);
}
public double PanningOffsetY
{
get => (double)GetValue(PanningOffsetYProperty);
set => SetValue(PanningOffsetYProperty, value);
}
public BitmapSource? ReferenceImage
{
get => (BitmapSource?)GetValue(ReferenceImageProperty);
set => SetValue(ReferenceImageProperty, value);
}
public double ImageScaleFactor
{
get => (double)GetValue(ImageScaleFactorProperty);
set => SetValue(ImageScaleFactorProperty, value);
}
public int MaxImageWidth
{
get => (int)GetValue(MaxImageWidthProperty);
set => SetValue(MaxImageWidthProperty, value);
}
public int MaxImageHeight
{
get => (int)GetValue(MaxImageHeightProperty);
set => SetValue(MaxImageHeightProperty, value);
}
public bool EnablePanning
{
get => (bool)GetValue(EnablePanningProperty);
set => SetValue(EnablePanningProperty, value);
}
private Canvas roiCanvas = new Canvas();
public Canvas RoiCanvas
{
get => roiCanvas;
set => roiCanvas = value;
}
#endregion
#region Events
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseUp;
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseDown;
public event EventHandler<MouseButtonEventArgs>? CanvasLeftMouseDown;
public event EventHandler<MouseEventArgs>? CanvasMouseMove;
public event EventHandler<MouseWheelEventArgs>? CanvasMouseWheel;
#endregion
public ImageCanvasControl()
{
InitializeComponent();
}
#region Private Methods
private static void ReferenceImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as ImageCanvasControl)?.OnReferenceImageChanged(e.NewValue as BitmapSource);
}
private void OnReferenceImageChanged(BitmapSource? bitmapSource)
{
if (bitmapSource != null)
{
ImageBrush brush = new ImageBrush { ImageSource = bitmapSource, Stretch = Stretch.Uniform };
RoiCanvas.Background = brush;
RoiCanvas.Height = bitmapSource.Height;
RoiCanvas.Width = bitmapSource.Width;
}
else
{
RoiCanvas.Height = MaxImageHeight > 0 ? MaxImageHeight : 600;
RoiCanvas.Width = MaxImageWidth > 0 ? MaxImageWidth : 800;
RoiCanvas.Background = Brushes.LightGray;
}
}
private double CalculateScaleFactor()
{
if (ActualWidth <= 0) return 1;
double scaleFactor = Math.Max(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
if (scaleFactor < 0)
scaleFactor = Math.Min(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
return scaleFactor;
}
#endregion
#region Event Handlers
private void Canvas_MouseEnter(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
mouseDownPoint = e.GetPosition(RoiCanvas);
}
private void Canvas_MouseMove(object sender, MouseEventArgs e)
{
CanvasMouseMove?.Invoke(sender, e);
if (EnablePanning && e.LeftButton == MouseButtonState.Pressed)
{
Point mousePoint = e.GetPosition(RoiCanvas);
double mouseMoveLength = Point.Subtract(mousePoint, mouseDownPoint).Length;
if (mouseMoveLength > (10 * CalculateScaleFactor()) / ZoomScale)
{
PanningOffsetX += mousePoint.X - mouseDownPoint.X;
PanningOffsetY += mousePoint.Y - mouseDownPoint.Y;
}
}
}
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { }
private void Canvas_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
CanvasRightMouseUp?.Invoke(sender, e);
}
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mouseDownPoint = e.GetPosition(RoiCanvas);
CanvasLeftMouseDown?.Invoke(sender, e);
if (EnablePanning && e.ClickCount == 2)
{
ResetView();
e.Handled = true;
}
}
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
CanvasRightMouseDown?.Invoke(sender, e);
e.Handled = true;
}
private void Adorner_MouseWheel(object sender, MouseWheelEventArgs e)
{
CanvasMouseWheel?.Invoke(sender, e);
}
private void ContentPresenter_SizeChanged(object sender, SizeChangedEventArgs e)
{
ImageScaleFactor = CalculateScaleFactor();
}
private void ResetView()
{
ZoomScale = 1.0;
PanningOffsetX = 0.0;
PanningOffsetY = 0.0;
ZoomCenter = new Point(0, 0);
}
#endregion
}
@@ -0,0 +1,73 @@
using System.Drawing;
using System.Windows.Media.Imaging;
namespace XP.Camera.Calibration;
/// <summary>
/// 标定采集服务接口
/// 提供"一键采集"能力:读取编码器坐标 + 拍图 + 识别标记中心
/// </summary>
public interface ICalibrationCaptureService
{
/// <summary>是否可用(相机已连接、运动系统就绪)</summary>
bool IsAvailable { get; }
/// <summary>
/// 采集当前标定点
/// </summary>
/// <returns>采集结果,失败时返回 null</returns>
CaptureResult? CaptureCurrentPoint();
/// <summary>
/// 获取当前导航相机图像
/// </summary>
BitmapSource? CaptureImage();
/// <summary>
/// 启动实时预览(将相机实时画面推送到 LiveImageUpdated 事件)
/// </summary>
void StartLivePreview();
/// <summary>
/// 停止实时预览
/// </summary>
void StopLivePreview();
/// <summary>
/// 实时画面更新事件
/// </summary>
event EventHandler<LiveImageEventArgs>? LiveImageUpdated;
}
/// <summary>
/// 实时画面事件参数
/// </summary>
public class LiveImageEventArgs : EventArgs
{
public BitmapSource Image { get; }
public LiveImageEventArgs(BitmapSource image) => Image = image;
}
/// <summary>
/// 单次采集结果
/// </summary>
public class CaptureResult
{
/// <summary>标记中心像素坐标 X(亚像素)</summary>
public double PixelX { get; set; }
/// <summary>标记中心像素坐标 Y(亚像素)</summary>
public double PixelY { get; set; }
/// <summary>平台编码器坐标 X (mm)</summary>
public double WorldX { get; set; }
/// <summary>平台编码器坐标 Y (mm)</summary>
public double WorldY { get; set; }
/// <summary>采集的图像</summary>
public BitmapSource? Image { get; set; }
/// <summary>检测到的标记轮廓点集</summary>
public System.Drawing.Point[]? ContourPoints { get; set; }
}
@@ -11,16 +11,20 @@ namespace XP.Camera.Calibration.ViewModels;
public class CalibrationViewModel : BindableBase
{
private readonly ICalibrationDialogService _dialogService;
private readonly ICalibrationCaptureService? _captureService;
private readonly CalibrationProcessor _calibrator = new();
private Image<Bgr, byte>? _currentImage;
private static readonly ILogger _logger = Log.ForContext<CalibrationViewModel>();
private BitmapSource? _imageSource;
private BitmapSource? _frozenImage;
private string _statusText = Res.CalibrationStatusReady;
private bool _showWorldCoordinates;
private bool _isLiveView = true;
public CalibrationViewModel(ICalibrationDialogService dialogService)
public CalibrationViewModel(ICalibrationDialogService dialogService, ICalibrationCaptureService? captureService = null)
{
_dialogService = dialogService;
_captureService = captureService;
CalibrationPoints = new ObservableCollection<CalibrationProcessor.CalibrationPoint>();
LoadImageCommand = new DelegateCommand(LoadImage);
@@ -29,6 +33,16 @@ public class CalibrationViewModel : BindableBase
.ObservesProperty(() => CalibrationPoints.Count);
SaveCalibrationCommand = new DelegateCommand(SaveCalibration);
LoadCalibrationCommand = new DelegateCommand(LoadCalibration);
CapturePointCommand = new DelegateCommand(CapturePoint, CanCapturePoint);
DeleteSelectedPointCommand = new DelegateCommand(DeleteSelectedPoint, () => SelectedPoint != null)
.ObservesProperty(() => SelectedPoint);
// 启动实时预览
if (_captureService != null)
{
_captureService.LiveImageUpdated += OnLiveImageUpdated;
_captureService.StartLivePreview();
}
}
public ObservableCollection<CalibrationProcessor.CalibrationPoint> CalibrationPoints { get; }
@@ -39,6 +53,14 @@ public class CalibrationViewModel : BindableBase
set => SetProperty(ref _imageSource, value);
}
private BitmapSource? _overlayImage;
/// <summary>叠加层图像(显示检测到的轮廓和中心点)</summary>
public BitmapSource? OverlayImage
{
get => _overlayImage;
set => SetProperty(ref _overlayImage, value);
}
public string StatusText
{
get => _statusText;
@@ -51,11 +73,48 @@ public class CalibrationViewModel : BindableBase
set => SetProperty(ref _showWorldCoordinates, value);
}
public bool IsLiveView
{
get => _isLiveView;
set
{
if (SetProperty(ref _isLiveView, value))
{
RaisePropertyChanged(nameof(LiveViewButtonText));
if (value)
{
// 切回实时:恢复实时预览
_captureService?.StartLivePreview();
}
else
{
// 切到当前:冻结当前帧
_frozenImage = _imageSource;
_captureService?.StopLivePreview();
}
}
}
}
public string LiveViewButtonText => _isLiveView ? "⏸ 冻结" : "▶ 实时";
public DelegateCommand LoadImageCommand { get; }
public DelegateCommand LoadCsvCommand { get; }
public DelegateCommand CalibrateCommand { get; }
public DelegateCommand SaveCalibrationCommand { get; }
public DelegateCommand LoadCalibrationCommand { get; }
public DelegateCommand CapturePointCommand { get; }
public DelegateCommand DeleteSelectedPointCommand { get; }
/// <summary>是否支持采集模式(有采集服务注入)</summary>
public bool IsCaptureAvailable => _captureService?.IsAvailable == true;
private CalibrationProcessor.CalibrationPoint? _selectedPoint;
public CalibrationProcessor.CalibrationPoint? SelectedPoint
{
get => _selectedPoint;
set => SetProperty(ref _selectedPoint, value);
}
private void LoadImage()
{
@@ -130,6 +189,59 @@ public class CalibrationViewModel : BindableBase
}
}
private bool CanCapturePoint() => _captureService?.IsAvailable == true;
private void CapturePoint()
{
if (_captureService == null) return;
try
{
var result = _captureService.CaptureCurrentPoint();
if (result == null)
{
StatusText = "采集失败:未能识别标记点,请确认标记在视野内";
return;
}
CalibrationPoints.Add(new CalibrationProcessor.CalibrationPoint
{
PixelX = result.PixelX,
PixelY = result.PixelY,
WorldX = result.WorldX,
WorldY = result.WorldY
});
// 更新图像显示
if (result.Image != null)
{
ImageSource = result.Image;
}
// 绘制检测结果叠加层(轮廓 + 中心点)
DrawDetectionOverlay(result);
StatusText = $"已采集第 {CalibrationPoints.Count} 个点: 像素({result.PixelX:F1}, {result.PixelY:F1}) → 物理({result.WorldX:F3}, {result.WorldY:F3})";
_logger.Information("标定点采集: Pixel=({PixelX:F1}, {PixelY:F1}), World=({WorldX:F3}, {WorldY:F3})",
result.PixelX, result.PixelY, result.WorldX, result.WorldY);
}
catch (Exception ex)
{
StatusText = $"采集异常: {ex.Message}";
_logger.Error(ex, "标定点采集失败");
}
}
private void DeleteSelectedPoint()
{
if (SelectedPoint != null && CalibrationPoints.Contains(SelectedPoint))
{
CalibrationPoints.Remove(SelectedPoint);
SelectedPoint = null;
StatusText = $"已删除,剩余 {CalibrationPoints.Count} 个标定点";
}
}
public PointF ConvertPixelToWorld(PointF pixel) => _calibrator.PixelToWorld(pixel);
public Image<Bgr, byte>? CurrentImage => _currentImage;
@@ -138,6 +250,82 @@ public class CalibrationViewModel : BindableBase
private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty);
private void OnLiveImageUpdated(object? sender, LiveImageEventArgs e)
{
if (!_isLiveView) return;
System.Windows.Application.Current?.Dispatcher?.BeginInvoke(() =>
{
ImageSource = e.Image;
});
}
/// <summary>
/// 绘制检测结果叠加层(轮廓 + 中心十字 + 坐标文字)
/// </summary>
private void DrawDetectionOverlay(CaptureResult result)
{
if (result.Image == null) return;
int w = result.Image.PixelWidth;
int h = result.Image.PixelHeight;
// 创建透明叠加层
var visual = new System.Windows.Media.DrawingVisual();
using (var dc = visual.RenderOpen())
{
// 绘制轮廓
if (result.ContourPoints != null && result.ContourPoints.Length > 2)
{
var pen = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Lime, 2);
var geometry = new System.Windows.Media.StreamGeometry();
using (var ctx = geometry.Open())
{
ctx.BeginFigure(new System.Windows.Point(result.ContourPoints[0].X, result.ContourPoints[0].Y), false, true);
for (int i = 1; i < result.ContourPoints.Length; i++)
ctx.LineTo(new System.Windows.Point(result.ContourPoints[i].X, result.ContourPoints[i].Y), true, false);
}
geometry.Freeze();
dc.DrawGeometry(null, pen, geometry);
}
// 绘制中心十字
double cx = result.PixelX;
double cy = result.PixelY;
double crossSize = Math.Max(10, Math.Max(w, h) / 80.0);
var crossPen = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Red, 2);
dc.DrawLine(crossPen, new System.Windows.Point(cx - crossSize, cy), new System.Windows.Point(cx + crossSize, cy));
dc.DrawLine(crossPen, new System.Windows.Point(cx, cy - crossSize), new System.Windows.Point(cx, cy + crossSize));
// 绘制坐标文字
var text = new System.Windows.Media.FormattedText(
$"({result.PixelX:F1}, {result.PixelY:F1})",
System.Globalization.CultureInfo.CurrentCulture,
System.Windows.FlowDirection.LeftToRight,
new System.Windows.Media.Typeface("Segoe UI"),
Math.Max(12, Math.Max(w, h) / 60.0),
System.Windows.Media.Brushes.Yellow,
1.0);
dc.DrawText(text, new System.Windows.Point(cx + crossSize + 4, cy - text.Height / 2));
}
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(w, h, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
rtb.Render(visual);
rtb.Freeze();
OverlayImage = rtb;
}
/// <summary>
/// 停止实时预览并清理资源(窗口关闭时调用)
/// </summary>
public void Cleanup()
{
if (_captureService != null)
{
_captureService.StopLivePreview();
_captureService.LiveImageUpdated -= OnLiveImageUpdated;
}
}
private static BitmapSource MatToBitmapSource(Mat mat)
{
using var bitmap = mat.ToBitmap();
+146 -1
View File
@@ -10,6 +10,7 @@ public static class PixelConverter
{
/// <summary>
/// 将原始像素数据转换为 WPF 的 BitmapSource 对象。
/// 支持 Mono8、BGR8、RGB8、BGRA8 以及 Bayer 8-bit 格式(自动解码为 BGR24)。
/// 返回的 BitmapSource 已调用 Freeze(),可跨线程访问。
/// </summary>
public static BitmapSource ToBitmapSource(byte[] pixelData, int width, int height, string pixelFormat)
@@ -19,11 +20,23 @@ public static class PixelConverter
if (height <= 0) throw new ArgumentException("Height must be a positive integer.", nameof(height));
ArgumentNullException.ThrowIfNull(pixelFormat);
var (format, stride) = pixelFormat switch
string normalized = NormalizePixelFormat(pixelFormat);
// Bayer 格式需要解码
if (normalized.StartsWith("Bayer"))
{
byte[] bgrData = DemosaicBayer(pixelData, width, height, normalized);
var bmp = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr24, null, bgrData, width * 3);
bmp.Freeze();
return bmp;
}
var (format, stride) = normalized switch
{
"Mono8" => (PixelFormats.Gray8, width),
"BGR8" => (PixelFormats.Bgr24, width * 3),
"BGRA8" => (PixelFormats.Bgra32, width * 4),
"RGB8" => (PixelFormats.Rgb24, width * 3),
_ => throw new NotSupportedException($"Pixel format '{pixelFormat}' is not supported.")
};
@@ -31,4 +44,136 @@ public static class PixelConverter
bitmap.Freeze();
return bitmap;
}
/// <summary>
/// 将不同 SDK 的像素格式名称统一为标准名称。
/// </summary>
private static string NormalizePixelFormat(string pixelFormat)
{
if (pixelFormat is "Mono8" or "BGR8" or "BGRA8" or "RGB8")
return pixelFormat;
var upper = pixelFormat.ToUpperInvariant();
if (upper.Contains("MONO8")) return "Mono8";
if (upper.Contains("BGR8")) return "BGR8";
if (upper.Contains("BGRA8")) return "BGRA8";
if (upper.Contains("RGB8") && !upper.Contains("BAYER")) return "RGB8";
// Bayer 格式
if (upper.Contains("BAYERRG8") || upper.Contains("BAYER_RG8")) return "BayerRG8";
if (upper.Contains("BAYERGR8") || upper.Contains("BAYER_GR8")) return "BayerGR8";
if (upper.Contains("BAYERGB8") || upper.Contains("BAYER_GB8")) return "BayerGB8";
if (upper.Contains("BAYERBG8") || upper.Contains("BAYER_BG8")) return "BayerBG8";
return pixelFormat;
}
/// <summary>
/// 简单 Bayer 解码(双线性插值),输出 BGR24。
/// </summary>
private static byte[] DemosaicBayer(byte[] bayer, int width, int height, string pattern)
{
// pattern: BayerRG8, BayerGR8, BayerGB8, BayerBG8
// RG: R G GR: G R GB: G B BG: B G
// G B B G R G G R
int rRow, rCol; // 红色像素在2x2块中的位置
switch (pattern)
{
case "BayerRG8": rRow = 0; rCol = 0; break;
case "BayerGR8": rRow = 0; rCol = 1; break;
case "BayerGB8": rRow = 1; rCol = 0; break;
case "BayerBG8": rRow = 1; rCol = 1; break;
default: rRow = 0; rCol = 0; break;
}
byte[] bgr = new byte[width * height * 3];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int srcIdx = y * width + x;
int dstIdx = (y * width + x) * 3;
// 确定当前像素在 Bayer 模式中的角色
int py = (y + rRow) % 2; // 0=红行, 1=蓝行
int px = (x + rCol) % 2; // 0=红列/蓝列, 1=绿列
byte r, g, b;
if (py == 0 && px == 0)
{
// 红色像素位置
r = bayer[srcIdx];
g = AvgNeighbors4(bayer, width, height, x, y);
b = AvgDiagonal(bayer, width, height, x, y);
}
else if (py == 1 && px == 1)
{
// 蓝色像素位置
b = bayer[srcIdx];
g = AvgNeighbors4(bayer, width, height, x, y);
r = AvgDiagonal(bayer, width, height, x, y);
}
else if (py == 0 && px == 1)
{
// 绿色像素(红行)
g = bayer[srcIdx];
r = AvgHorizontal(bayer, width, x, y);
b = AvgVertical(bayer, width, height, x, y);
}
else
{
// 绿色像素(蓝行)
g = bayer[srcIdx];
b = AvgHorizontal(bayer, width, x, y);
r = AvgVertical(bayer, width, height, x, y);
}
bgr[dstIdx] = b;
bgr[dstIdx + 1] = g;
bgr[dstIdx + 2] = r;
}
}
return bgr;
}
private static byte AvgNeighbors4(byte[] data, int w, int h, int x, int y)
{
int sum = 0, count = 0;
if (x > 0) { sum += data[y * w + x - 1]; count++; }
if (x < w - 1) { sum += data[y * w + x + 1]; count++; }
if (y > 0) { sum += data[(y - 1) * w + x]; count++; }
if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; }
return count > 0 ? (byte)(sum / count) : (byte)0;
}
private static byte AvgDiagonal(byte[] data, int w, int h, int x, int y)
{
int sum = 0, count = 0;
if (x > 0 && y > 0) { sum += data[(y - 1) * w + x - 1]; count++; }
if (x < w - 1 && y > 0) { sum += data[(y - 1) * w + x + 1]; count++; }
if (x > 0 && y < h - 1) { sum += data[(y + 1) * w + x - 1]; count++; }
if (x < w - 1 && y < h - 1) { sum += data[(y + 1) * w + x + 1]; count++; }
return count > 0 ? (byte)(sum / count) : (byte)0;
}
private static byte AvgHorizontal(byte[] data, int w, int x, int y)
{
int sum = 0, count = 0;
if (x > 0) { sum += data[y * w + x - 1]; count++; }
if (x < w - 1) { sum += data[y * w + x + 1]; count++; }
return count > 0 ? (byte)(sum / count) : (byte)0;
}
private static byte AvgVertical(byte[] data, int w, int h, int x, int y)
{
int sum = 0, count = 0;
if (y > 0) { sum += data[(y - 1) * w + x]; count++; }
if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; }
return count > 0 ? (byte)(sum / count) : (byte)0;
}
}
+2 -2
View File
@@ -11,8 +11,8 @@ public class CameraFactory : ICameraFactory
return cameraType switch
{
"Basler" => new BaslerCameraController(),
// "Hikvision" => new HikvisionCameraController(),
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
"Hikvision" => new HikvisionCameraController(),
_ => throw new NotSupportedException($"Unsupported Camera Type: {cameraType}")
};
}
}
@@ -0,0 +1,535 @@
using MvCameraControl;
using Serilog;
namespace XP.Camera;
/// <summary>
/// 海康威视相机控制器,封装 MvCameraControl.Net SDK 实现 <see cref="ICameraController"/>。
/// </summary>
/// <remarks>
/// <para>所有公共方法通过内部 <c>_syncLock</c> 对象进行 lock 同步,保证线程安全。</para>
/// <para>事件回调(ImageGrabbed、GrabError)在 SDK 回调线程上触发,不持有 _syncLock,避免死锁。</para>
/// </remarks>
public class HikvisionCameraController : ICameraController
{
private static readonly ILogger _logger = Log.ForContext<HikvisionCameraController>();
private static bool _sdkInitialized;
private static readonly object _sdkInitLock = new();
private readonly object _syncLock = new();
private IDevice? _device;
private CameraInfo? _cachedCameraInfo;
private bool _isConnected;
private bool _isGrabbing;
public HikvisionCameraController()
{
// SDK 初始化延迟到 Open() 中执行
}
/// <inheritdoc />
public bool IsConnected
{
get { lock (_syncLock) { return _isConnected; } }
}
/// <inheritdoc />
public bool IsGrabbing
{
get { lock (_syncLock) { return _isGrabbing; } }
}
/// <inheritdoc />
public event EventHandler<ImageGrabbedEventArgs>? ImageGrabbed;
/// <inheritdoc />
public event EventHandler<GrabErrorEventArgs>? GrabError;
/// <inheritdoc />
public event EventHandler? ConnectionLost;
/// <inheritdoc />
public CameraInfo Open()
{
lock (_syncLock)
{
if (_isConnected && _cachedCameraInfo != null)
{
_logger.Information("Hikvision camera already connected, returning cached info.");
return _cachedCameraInfo;
}
try
{
_logger.Information("Opening Hikvision camera connection...");
// 确保 SDK 初始化
EnsureSdkInitialized();
// 枚举设备
DeviceTLayerType layerType = DeviceTLayerType.MvGigEDevice
| DeviceTLayerType.MvUsbDevice;
List<IDeviceInfo> deviceInfoList;
int ret = DeviceEnumerator.EnumDevices(layerType, out deviceInfoList);
_logger.Information("EnumDevices(GigE|USB) returned: 0x{RetCode:X8}, device count: {Count}",
ret, deviceInfoList?.Count ?? 0);
// 如果没找到,分别尝试
if (ret == MvError.MV_OK && (deviceInfoList == null || deviceInfoList.Count == 0))
{
// 单独尝试 GigE
List<IDeviceInfo> gigeList;
int retGige = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvGigEDevice, out gigeList);
_logger.Information("EnumDevices(GigE only) returned: 0x{RetCode:X8}, count: {Count}",
retGige, gigeList?.Count ?? 0);
// 单独尝试 USB
List<IDeviceInfo> usbList;
int retUsb = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvUsbDevice, out usbList);
_logger.Information("EnumDevices(USB only) returned: 0x{RetCode:X8}, count: {Count}",
retUsb, usbList?.Count ?? 0);
// 合并结果
deviceInfoList = new List<IDeviceInfo>();
if (gigeList != null) deviceInfoList.AddRange(gigeList);
if (usbList != null) deviceInfoList.AddRange(usbList);
}
if (ret != MvError.MV_OK)
{
throw new CameraException($"Enumerate Hikvision devices failed: 0x{ret:X8}");
}
if (deviceInfoList == null || deviceInfoList.Count == 0)
{
throw new DeviceNotFoundException("No Hikvision camera device found.");
}
// 选择第一个设备
IDeviceInfo deviceInfo = deviceInfoList[0];
_logger.Information("Found Hikvision device: {Model} (SN: {Serial})",
deviceInfo.ModelName, deviceInfo.SerialNumber);
// 创建设备
_device = DeviceFactory.CreateDevice(deviceInfo);
// 打开设备
ret = _device.Open();
if (ret != MvError.MV_OK)
{
_device.Dispose();
_device = null;
throw new CameraException($"Open Hikvision device failed: 0x{ret:X8}");
}
// GigE 设备优化包大小
if (_device is IGigEDevice gigEDevice)
{
int packetSize;
ret = gigEDevice.GetOptimalPacketSize(out packetSize);
if (ret == MvError.MV_OK && packetSize > 0)
{
_device.Parameters.SetIntValue("GevSCPSPacketSize", packetSize);
_logger.Debug("Set GigE packet size to {PacketSize}", packetSize);
}
}
// 配置软件触发模式
_device.Parameters.SetEnumValueByString("TriggerMode", "On");
_device.Parameters.SetEnumValueByString("TriggerSource", "Software");
// 彩色相机:尝试设置输出为 BGR8 以便直接显示
// 如果相机不支持 BGR8(如只支持 Bayer),则保持默认
int fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "BGR8Packed");
if (fmtRet != MvError.MV_OK)
{
// 尝试 Mono8(黑白相机)
fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "Mono8");
}
_logger.Debug("Set PixelFormat result: 0x{Ret:X8}", fmtRet);
_cachedCameraInfo = new CameraInfo(
ModelName: deviceInfo.ModelName ?? "",
SerialNumber: deviceInfo.SerialNumber ?? "",
VendorName: deviceInfo.ManufacturerName ?? "",
DeviceType: deviceInfo.TLayerType.ToString()
);
_isConnected = true;
_logger.Information("Hikvision camera connected: {ModelName} (SN: {SerialNumber})",
_cachedCameraInfo.ModelName, _cachedCameraInfo.SerialNumber);
return _cachedCameraInfo;
}
catch (Exception ex) when (ex is not CameraException)
{
_device?.Dispose();
_device = null;
_logger.Error(ex, "Failed to open Hikvision camera.");
throw new CameraException("Failed to open Hikvision camera device.", ex);
}
}
}
/// <inheritdoc />
public void Close()
{
lock (_syncLock)
{
if (!_isConnected)
{
_logger.Information("Hikvision camera not connected, Close() ignored.");
return;
}
try
{
if (_isGrabbing)
{
StopGrabbingInternal();
}
_logger.Information("Closing Hikvision camera connection...");
_device?.Close();
_device?.Dispose();
_device = null;
_isConnected = false;
_cachedCameraInfo = null;
_logger.Information("Hikvision camera connection closed.");
}
catch (Exception ex) when (ex is not CameraException)
{
_device = null;
_isConnected = false;
_isGrabbing = false;
_cachedCameraInfo = null;
_logger.Error(ex, "Error while closing Hikvision camera.");
throw new CameraException("Failed to close Hikvision camera device.", ex);
}
}
}
/// <inheritdoc />
public void StartGrabbing()
{
lock (_syncLock)
{
EnsureConnected();
if (_isGrabbing)
{
_logger.Information("Already grabbing, StartGrabbing() ignored.");
return;
}
try
{
_logger.Information("Starting Hikvision grabbing with software trigger...");
// 设置缓存节点数
_device!.StreamGrabber.SetImageNodeNum(5);
// 注册回调
_device.StreamGrabber.FrameGrabedEvent += OnFrameGrabbed;
// 开始采集
int ret = _device.StreamGrabber.StartGrabbing();
if (ret != MvError.MV_OK)
{
_device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed;
throw new CameraException($"Start grabbing failed: 0x{ret:X8}");
}
_isGrabbing = true;
_logger.Information("Hikvision grabbing started.");
}
catch (Exception ex) when (ex is not CameraException)
{
_logger.Error(ex, "Failed to start Hikvision grabbing.");
throw new CameraException("Failed to start grabbing.", ex);
}
}
}
/// <inheritdoc />
public void ExecuteSoftwareTrigger()
{
lock (_syncLock)
{
if (!_isGrabbing)
{
throw new InvalidOperationException("Cannot execute software trigger: camera is not grabbing.");
}
try
{
int ret = _device!.Parameters.SetCommandValue("TriggerSoftware");
if (ret != MvError.MV_OK)
{
throw new CameraException($"Execute software trigger failed: 0x{ret:X8}");
}
}
catch (Exception ex) when (ex is not CameraException and not InvalidOperationException)
{
_logger.Error(ex, "Failed to execute software trigger.");
throw new CameraException("Failed to execute software trigger.", ex);
}
}
}
/// <inheritdoc />
public void StopGrabbing()
{
lock (_syncLock)
{
if (!_isGrabbing) return;
StopGrabbingInternal();
}
}
/// <inheritdoc />
public double GetExposureTime()
{
lock (_syncLock)
{
EnsureConnected();
IFloatValue floatValue;
int ret = _device!.Parameters.GetFloatValue("ExposureTime", out floatValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get ExposureTime failed: 0x{ret:X8}");
return floatValue.CurValue;
}
}
/// <inheritdoc />
public void SetExposureTime(double microseconds)
{
lock (_syncLock)
{
EnsureConnected();
// 关闭自动曝光
_device!.Parameters.SetEnumValueByString("ExposureAuto", "Off");
int ret = _device.Parameters.SetFloatValue("ExposureTime", (float)microseconds);
if (ret != MvError.MV_OK)
throw new CameraException($"Set ExposureTime failed: 0x{ret:X8}");
_logger.Information("Hikvision exposure time set to {Microseconds} µs.", microseconds);
}
}
/// <inheritdoc />
public double GetGain()
{
lock (_syncLock)
{
EnsureConnected();
IFloatValue floatValue;
int ret = _device!.Parameters.GetFloatValue("Gain", out floatValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get Gain failed: 0x{ret:X8}");
return floatValue.CurValue;
}
}
/// <inheritdoc />
public void SetGain(double value)
{
lock (_syncLock)
{
EnsureConnected();
_device!.Parameters.SetEnumValueByString("GainAuto", "Off");
int ret = _device.Parameters.SetFloatValue("Gain", (float)value);
if (ret != MvError.MV_OK)
throw new CameraException($"Set Gain failed: 0x{ret:X8}");
_logger.Information("Hikvision gain set to {Value}.", value);
}
}
/// <inheritdoc />
public int GetWidth()
{
lock (_syncLock)
{
EnsureConnected();
IIntValue intValue;
int ret = _device!.Parameters.GetIntValue("Width", out intValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get Width failed: 0x{ret:X8}");
return (int)intValue.CurValue;
}
}
/// <inheritdoc />
public void SetWidth(int value)
{
lock (_syncLock)
{
EnsureConnected();
int ret = _device!.Parameters.SetIntValue("Width", value);
if (ret != MvError.MV_OK)
throw new CameraException($"Set Width failed: 0x{ret:X8}");
_logger.Information("Hikvision width set to {Value}.", value);
}
}
/// <inheritdoc />
public int GetHeight()
{
lock (_syncLock)
{
EnsureConnected();
IIntValue intValue;
int ret = _device!.Parameters.GetIntValue("Height", out intValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get Height failed: 0x{ret:X8}");
return (int)intValue.CurValue;
}
}
/// <inheritdoc />
public void SetHeight(int value)
{
lock (_syncLock)
{
EnsureConnected();
int ret = _device!.Parameters.SetIntValue("Height", value);
if (ret != MvError.MV_OK)
throw new CameraException($"Set Height failed: 0x{ret:X8}");
_logger.Information("Hikvision height set to {Value}.", value);
}
}
/// <inheritdoc />
public string GetPixelFormat()
{
lock (_syncLock)
{
EnsureConnected();
IEnumValue enumValue;
int ret = _device!.Parameters.GetEnumValue("PixelFormat", out enumValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get PixelFormat failed: 0x{ret:X8}");
return enumValue.CurEnumEntry.Symbolic;
}
}
/// <inheritdoc />
public void SetPixelFormat(string format)
{
lock (_syncLock)
{
EnsureConnected();
int ret = _device!.Parameters.SetEnumValueByString("PixelFormat", format);
if (ret != MvError.MV_OK)
throw new CameraException($"Set PixelFormat failed: 0x{ret:X8}");
_logger.Information("Hikvision pixel format set to {Format}.", format);
}
}
/// <inheritdoc />
public void Dispose()
{
Close();
GC.SuppressFinalize(this);
}
// ══════════════════════════════════════════════════════════════
// 私有方法
// ══════════════════════════════════════════════════════════════
/// <summary>
/// SDK 回调:图像采集完成
/// </summary>
private void OnFrameGrabbed(object? sender, FrameGrabbedEventArgs e)
{
try
{
var frameOut = e.FrameOut;
if (frameOut == null || frameOut.Image == null)
{
_logger.Warning("Hikvision OnFrameGrabbed: FrameOut or Image is null");
GrabError?.Invoke(this, new GrabErrorEventArgs(-1, "FrameOut or Image is null."));
return;
}
var image = frameOut.Image;
int width = (int)image.Width;
int height = (int)image.Height;
int imageSize = (int)image.ImageSize;
string pixelFormat = image.PixelType.ToString();
// 提取像素数据
byte[] pixelData = image.PixelData ?? Array.Empty<byte>();
_logger.Debug("Hikvision frame: {Width}x{Height}, format={Format}, dataLen={Len}",
width, height, pixelFormat, pixelData.Length);
if (pixelData.Length == 0)
{
_logger.Warning("Hikvision OnFrameGrabbed: PixelData is empty");
return;
}
var args = new ImageGrabbedEventArgs(pixelData, width, height, pixelFormat);
ImageGrabbed?.Invoke(this, args);
}
catch (Exception ex)
{
_logger.Error(ex, "Exception in Hikvision OnFrameGrabbed handler.");
}
}
private void StopGrabbingInternal()
{
if (!_isGrabbing) return;
try
{
_device?.StreamGrabber.StopGrabbing();
if (_device != null)
_device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed;
_isGrabbing = false;
_logger.Information("Hikvision grabbing stopped.");
}
catch (Exception ex) when (ex is not CameraException)
{
_isGrabbing = false;
_logger.Error(ex, "Error while stopping Hikvision grabbing.");
throw new CameraException("Failed to stop grabbing.", ex);
}
}
private void EnsureConnected()
{
if (!_isConnected)
throw new InvalidOperationException("Hikvision camera is not connected. Call Open() first.");
}
/// <summary>
/// 确保 SDK 全局初始化(只调用一次)
/// </summary>
private static void EnsureSdkInitialized()
{
if (_sdkInitialized) return;
lock (_sdkInitLock)
{
if (_sdkInitialized) return;
try
{
int ret = SDKSystem.Initialize();
if (ret != MvError.MV_OK)
{
_logger.Error("Hikvision SDK Initialize failed: 0x{ErrorCode:X8}", ret);
throw new CameraException($"Hikvision SDK Initialize failed: 0x{ret:X8}");
}
_sdkInitialized = true;
_logger.Information("Hikvision SDK initialized successfully.");
}
catch (Exception ex) when (ex is not CameraException)
{
_logger.Error(ex, "Failed to initialize Hikvision SDK.");
throw new CameraException("Failed to initialize Hikvision SDK.", ex);
}
}
}
}
+8
View File
@@ -7,18 +7,26 @@
<Nullable>enable</Nullable>
<RootNamespace>XP.Camera</RootNamespace>
<AssemblyName>XP.Camera</AssemblyName>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
<Reference Include="Basler.Pylon">
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
</Reference>
<Reference Include="MvCameraControl.Net">
<HintPath>..\ExternalLibraries\MvCameraControl.Net.dll</HintPath>
<Private>true</Private>
<CopyLocal>true</CopyLocal>
</Reference>
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
<PackageReference Include="Emgu.CV.Bitmap" Version="4.10.0.5680" />
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
<PackageReference Include="Serilog" Version="4.3.1" />
<ProjectReference Include="..\XP.Common\XP.Common.csproj" />
<ProjectReference Include="..\XP.ImageProcessing.RoiControl\XP.ImageProcessing.RoiControl.csproj" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Telerik.Windows.Controls;
using Telerik.Windows.Controls.ChartView;
namespace XP.Common.Controls.ImageHistogram
{
/// <summary>
/// RadChartView 渲染适配器 | RadChartView rendering adapter
/// 负责将直方图频次数据渲染到 Telerik RadCartesianChart 控件
/// </summary>
internal sealed class ChartRenderer
{
private readonly RadCartesianChart _chart;
private readonly ScatterAreaSeries _areaSeries;
private readonly LinearAxis _xAxis;
/// <summary>
/// 16 位数据聚合因子 | 16-bit data aggregation factor
/// </summary>
private const int AggregationFactor = 256;
/// <summary>
/// 构造函数,接收 RadCartesianChart 实例 | Constructor, receives RadCartesianChart instance
/// </summary>
/// <param name="chart">图表控件实例 | Chart control instance</param>
/// <param name="areaSeries">面积图系列 | Area series</param>
/// <param name="xAxis">X 轴 | X axis</param>
public ChartRenderer(RadCartesianChart chart, ScatterAreaSeries areaSeries, LinearAxis xAxis)
{
_chart = chart ?? throw new ArgumentNullException(nameof(chart));
_areaSeries = areaSeries ?? throw new ArgumentNullException(nameof(areaSeries));
_xAxis = xAxis ?? throw new ArgumentNullException(nameof(xAxis));
}
/// <summary>
/// 更新直方图数据 | Update histogram data
/// </summary>
/// <param name="histogram">频次数组(256 或 65536 长度)| Frequency array (256 or 65536 length)</param>
/// <param name="isLogarithmic">是否使用对数 Y 轴 | Whether to use logarithmic Y axis</param>
public void UpdateData(long[] histogram, bool isLogarithmic)
{
if (histogram == null || histogram.Length == 0)
return;
// 确定是否需要聚合(16 位数据)| Determine if aggregation needed (16-bit data)
long[] displayData;
int xAxisMax;
if (histogram.Length == 65536)
{
// 16 位数据聚合为 256 个柱体 | Aggregate 16-bit data to 256 bars
displayData = Aggregate16BitHistogram(histogram);
xAxisMax = 65535;
}
else
{
// 8 位数据直接显示 | Display 8-bit data directly
displayData = histogram;
xAxisMax = 255;
}
// 设置 X 轴范围 | Set X axis range
_xAxis.Minimum = 0;
_xAxis.Maximum = xAxisMax;
// 根据范围自动设置刻度间隔(保持 4-5 个刻度)| Auto set major step based on range (keep 4-5 ticks)
_xAxis.MajorStep = xAxisMax <= 255 ? 64 : 16384;
// 构建数据点 | Build data points
var dataPoints = new List<HistogramDataPoint>();
if (isLogarithmic)
{
// 对数模式:频次为 0 的不绘制 | Logarithmic mode: skip zero frequency
for (int i = 0; i < displayData.Length; i++)
{
if (displayData[i] > 0)
{
double xValue = histogram.Length == 65536
? i * AggregationFactor + AggregationFactor / 2.0
: i;
dataPoints.Add(new HistogramDataPoint
{
GrayLevel = xValue,
Frequency = displayData[i]
});
}
}
}
else
{
// 线性模式:所有灰度级别都绘制 | Linear mode: draw all gray levels
for (int i = 0; i < displayData.Length; i++)
{
double xValue = histogram.Length == 65536
? i * AggregationFactor + AggregationFactor / 2.0
: i;
dataPoints.Add(new HistogramDataPoint
{
GrayLevel = xValue,
Frequency = displayData[i]
});
}
}
// 更新图表数据 | Update chart data
_areaSeries.ItemsSource = dataPoints;
// 设置 Y 轴范围 | Set Y axis range
UpdateYAxis(displayData, isLogarithmic);
}
/// <summary>
/// 清空图表,恢复初始状态 | Clear chart, restore initial state
/// </summary>
public void Clear()
{
_areaSeries.ItemsSource = null;
// X 轴范围重置为 0-255 | Reset X axis range to 0-255
_xAxis.Minimum = 0;
_xAxis.Maximum = 255;
// Y 轴范围重置为 0-1 | Reset Y axis range to 0-1
SetYAxisRange(0, 1, isLogarithmic: false);
}
/// <summary>
/// 获取当前数据点数量 | Get current data point count
/// </summary>
public int DataPointCount
{
get
{
if (_areaSeries.ItemsSource is ICollection<HistogramDataPoint> collection)
return collection.Count;
if (_areaSeries.ItemsSource is IEnumerable<HistogramDataPoint> enumerable)
return enumerable.Count();
return 0;
}
}
/// <summary>
/// 将 65536 长度的频次数组聚合为 256 个柱体 | Aggregate 65536-length array to 256 bars
/// </summary>
private static long[] Aggregate16BitHistogram(long[] histogram)
{
var aggregated = new long[256];
for (int i = 0; i < 256; i++)
{
long sum = 0;
int startIndex = i * AggregationFactor;
for (int j = 0; j < AggregationFactor; j++)
{
sum += histogram[startIndex + j];
}
aggregated[i] = sum;
}
return aggregated;
}
/// <summary>
/// 更新 Y 轴范围 | Update Y axis range
/// </summary>
private void UpdateYAxis(long[] displayData, bool isLogarithmic)
{
long maxValue = 0;
for (int i = 0; i < displayData.Length; i++)
{
if (displayData[i] > maxValue)
maxValue = displayData[i];
}
if (maxValue == 0)
maxValue = 1;
// 计算取整的 MajorStep(约 4 个刻度,对齐到 K/M 整数倍)| Calculate rounded MajorStep (~4 ticks, aligned to K/M multiples)
if (_chart.VerticalAxis is LinearAxis linearAxis)
{
long rawStep = maxValue / 4;
long step = RoundStepToNice(rawStep);
if (step < 1) step = 1;
linearAxis.MajorStep = step;
// 将最大值向上取整到 step 的整数倍 | Round max up to multiple of step
long roundedMax = ((maxValue / step) + 1) * step;
SetYAxisRange(0, roundedMax, isLogarithmic);
}
else
{
SetYAxisRange(0, maxValue, isLogarithmic);
}
}
/// <summary>
/// 将步长取整为"好看"的数值(1, 2, 5 的倍数 × 10^n| Round step to "nice" value (multiples of 1, 2, 5 × 10^n)
/// 例如:123456 → 100000350000 → 500000780000 → 1000000
/// </summary>
private static long RoundStepToNice(long rawStep)
{
if (rawStep <= 0) return 1;
// 找到数量级 | Find order of magnitude
double magnitude = Math.Pow(10, Math.Floor(Math.Log10(rawStep)));
double normalized = rawStep / magnitude;
// 取整到 1, 2, 5 中最近的 | Round to nearest of 1, 2, 5
double niceNormalized;
if (normalized <= 1.5)
niceNormalized = 1;
else if (normalized <= 3.5)
niceNormalized = 2;
else if (normalized <= 7.5)
niceNormalized = 5;
else
niceNormalized = 10;
return (long)(niceNormalized * magnitude);
}
/// <summary>
/// 设置 Y 轴范围和刻度类型 | Set Y axis range and scale type
/// </summary>
private void SetYAxisRange(double minimum, double maximum, bool isLogarithmic)
{
// 获取或创建 Y 轴 | Get or create Y axis
var verticalAxis = _chart.VerticalAxis;
if (isLogarithmic)
{
// 对数刻度 | Logarithmic scale
if (verticalAxis is LogarithmicAxis logAxis)
{
logAxis.Minimum = 1;
logAxis.Maximum = maximum;
logAxis.LogarithmBase = 10;
}
else
{
// 需要切换为对数轴 | Need to switch to logarithmic axis
var newLogAxis = new LogarithmicAxis
{
Minimum = 1,
Maximum = maximum,
LogarithmBase = 10
};
_chart.VerticalAxis = newLogAxis;
}
}
else
{
// 线性刻度 | Linear scale
if (verticalAxis is LinearAxis linearAxis)
{
linearAxis.Minimum = minimum;
linearAxis.Maximum = maximum;
}
else
{
// 需要切换为线性轴 | Need to switch to linear axis
var newLinearAxis = new LinearAxis
{
Minimum = minimum,
Maximum = maximum
};
_chart.VerticalAxis = newLinearAxis;
}
}
}
}
/// <summary>
/// 直方图数据点模型 | Histogram data point model
/// </summary>
internal class HistogramDataPoint
{
/// <summary>
/// 灰度级别(X 轴值)| Gray level (X axis value)
/// </summary>
public double GrayLevel { get; set; }
/// <summary>
/// 像素频次(Y 轴值)| Pixel frequency (Y axis value)
/// </summary>
public long Frequency { get; set; }
}
}
@@ -0,0 +1,207 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace XP.Common.Controls.ImageHistogram
{
/// <summary>
/// 帧率限流器,确保计算频率不超过 MaxFrameRate | Frame rate throttler
/// 支持从任意线程调用,使用 lock 保护内部状态
/// </summary>
internal sealed class FrameThrottler : IDisposable
{
private readonly object _lock = new();
private DateTime _lastProcessTime = DateTime.MinValue;
private Action? _pendingAction;
private CancellationTokenSource? _delayCts;
private bool _isProcessing;
private bool _disposed;
private int _maxFrameRate = 15;
/// <summary>
/// 最大刷新帧率(fps),有效范围 1-60,超出范围自动钳位 | Max frame rate (fps), valid range 1-60, auto-clamped
/// </summary>
public int MaxFrameRate
{
get => _maxFrameRate;
set => _maxFrameRate = Math.Clamp(value, 1, 60);
}
/// <summary>
/// 获取当前帧间隔(毫秒)| Get current frame interval (ms)
/// </summary>
private double FrameIntervalMs => 1000.0 / _maxFrameRate;
/// <summary>
/// 提交一帧计算动作 | Submit a frame compute action
/// 若未超过帧率限制则立即执行,否则缓存最新帧并延迟触发
/// </summary>
/// <param name="computeAction">计算动作 | Compute action</param>
/// <returns>是否被立即接受处理 | Whether it was immediately accepted</returns>
public bool TrySubmit(Action computeAction)
{
if (_disposed || computeAction == null)
return false;
lock (_lock)
{
var now = DateTime.UtcNow;
var elapsed = (now - _lastProcessTime).TotalMilliseconds;
if (elapsed >= FrameIntervalMs && !_isProcessing)
{
// 已超过间隔且无正在处理的任务,立即执行 | Interval exceeded and no processing, execute immediately
_isProcessing = true;
_lastProcessTime = now;
ExecuteAction(computeAction);
return true;
}
else
{
// 未超过间隔或正在处理中,缓存最新帧(丢弃之前的中间帧)| Cache latest frame, discard previous
_pendingAction = computeAction;
ScheduleDelayedExecution(elapsed);
return false;
}
}
}
/// <summary>
/// 执行计算动作(异步,完成后检查待处理帧)| Execute compute action asynchronously
/// </summary>
private void ExecuteAction(Action action)
{
Task.Run(() =>
{
try
{
action.Invoke();
}
catch
{
// 异常不外抛 | Do not propagate exceptions
}
finally
{
OnActionCompleted();
}
});
}
/// <summary>
/// 计算动作完成后的回调 | Callback after compute action completes
/// </summary>
private void OnActionCompleted()
{
Action? nextAction = null;
lock (_lock)
{
_isProcessing = false;
if (_pendingAction != null && !_disposed)
{
var now = DateTime.UtcNow;
var elapsed = (now - _lastProcessTime).TotalMilliseconds;
if (elapsed >= FrameIntervalMs)
{
// 间隔已到,立即执行待处理帧 | Interval reached, execute pending frame
nextAction = _pendingAction;
_pendingAction = null;
_isProcessing = true;
_lastProcessTime = now;
}
// 否则等待延迟触发 | Otherwise wait for delayed trigger
}
}
if (nextAction != null)
{
ExecuteAction(nextAction);
}
}
/// <summary>
/// 安排延迟执行(等待帧间隔到期后处理最新帧)| Schedule delayed execution
/// </summary>
private void ScheduleDelayedExecution(double elapsedMs)
{
// 取消之前的延迟任务 | Cancel previous delay task
_delayCts?.Cancel();
_delayCts?.Dispose();
_delayCts = new CancellationTokenSource();
var token = _delayCts.Token;
var delayMs = Math.Max(0, FrameIntervalMs - elapsedMs);
Task.Run(async () =>
{
try
{
await Task.Delay((int)delayMs, token);
if (token.IsCancellationRequested)
return;
Action? actionToExecute = null;
lock (_lock)
{
if (_pendingAction != null && !_isProcessing && !_disposed)
{
actionToExecute = _pendingAction;
_pendingAction = null;
_isProcessing = true;
_lastProcessTime = DateTime.UtcNow;
}
}
if (actionToExecute != null)
{
ExecuteAction(actionToExecute);
}
}
catch (OperationCanceledException)
{
// 延迟被取消,正常情况 | Delay cancelled, normal case
}
catch
{
// 异常不外抛 | Do not propagate exceptions
}
});
}
/// <summary>
/// 取消所有待处理任务 | Cancel all pending tasks
/// </summary>
public void Cancel()
{
lock (_lock)
{
_pendingAction = null;
_delayCts?.Cancel();
_delayCts?.Dispose();
_delayCts = null;
}
}
/// <summary>
/// 释放所有资源 | Dispose all resources
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
lock (_lock)
{
_pendingAction = null;
_delayCts?.Cancel();
_delayCts?.Dispose();
_delayCts = null;
}
}
}
}
@@ -0,0 +1,49 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace XP.Common.Controls.ImageHistogram
{
/// <summary>
/// 频次标签转换器:将大数值转为 K/M 缩写格式 | Frequency label converter: converts large values to K/M abbreviation format
/// 例如:500000 → "500K"1500000 → "1.5M"800 → "800"
/// </summary>
internal sealed class FrequencyLabelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null) return "0";
double num;
if (value is double d)
num = d;
else if (value is decimal dec)
num = (double)dec;
else if (!double.TryParse(value.ToString(), out num))
return value.ToString() ?? "0";
if (num >= 1_000_000)
{
double mValue = num / 1_000_000.0;
return mValue == Math.Floor(mValue)
? $"{(int)mValue}M"
: $"{mValue:0.#}M";
}
if (num >= 1_000)
{
double kValue = num / 1_000.0;
return kValue == Math.Floor(kValue)
? $"{(int)kValue}K"
: $"{kValue:0.#}K";
}
return $"{(int)num}";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,211 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace XP.Common.Controls.ImageHistogram
{
/// <summary>
/// 直方图后台计算引擎 | Histogram background computation engine
/// 负责在后台线程中执行灰度值遍历和统计计算
/// </summary>
internal sealed class HistogramEngine : IDisposable
{
/// <summary>
/// 单帧计算超时时间(毫秒)| Single frame computation timeout (ms)
/// </summary>
private const int ComputeTimeoutMs = 5000;
private CancellationTokenSource? _timeoutCts;
private readonly object _lock = new();
private bool _disposed;
/// <summary>
/// 从 Image&lt;Rgba32&gt; 计算灰度直方图 | Compute histogram from Image&lt;Rgba32&gt;
/// 使用 ITU-R BT.601 亮度公式:Gray = 0.299R + 0.587G + 0.114B
/// </summary>
/// <param name="image">输入图像 | Input image</param>
/// <param name="ct">取消令牌 | Cancellation token</param>
/// <returns>256 长度的频次数组,失败返回 null | 256-length frequency array, null on failure</returns>
public Task<long[]?> ComputeAsync(Image<Rgba32> image, CancellationToken ct)
{
if (image == null)
return Task.FromResult<long[]?>(null);
// 创建超时令牌 | Create timeout token
var linkedCts = CreateLinkedTimeoutToken(ct);
var linkedToken = linkedCts.Token;
return Task.Run(() =>
{
try
{
var width = image.Width;
var height = image.Height;
var histogram = new long[256];
// 遍历像素,使用亮度公式计算灰度值 | Iterate pixels, compute grayscale using luminance formula
for (int y = 0; y < height; y++)
{
linkedToken.ThrowIfCancellationRequested();
for (int x = 0; x < width; x++)
{
var pixel = image[x, y];
// ITU-R BT.601 亮度公式 | ITU-R BT.601 luminance formula
var gray = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B);
// 钳位到 0-255 范围 | Clamp to 0-255 range
gray = Math.Clamp(gray, 0, 255);
histogram[gray]++;
}
}
return (long[]?)histogram;
}
catch (OperationCanceledException)
{
// 超时或取消,返回 null | Timeout or cancelled, return null
return null;
}
catch
{
// 所有异常内部捕获,不向外抛出 | Catch all exceptions internally
return null;
}
finally
{
linkedCts.Dispose();
}
}, linkedToken);
}
/// <summary>
/// 从原始字节数组计算灰度直方图 | Compute histogram from raw byte array
/// </summary>
/// <param name="rawData">原始像素数据 | Raw pixel data</param>
/// <param name="width">图像宽度 | Image width</param>
/// <param name="height">图像高度 | Image height</param>
/// <param name="bitDepth">位深度(8 或 16| Bit depth (8 or 16)</param>
/// <param name="ct">取消令牌 | Cancellation token</param>
/// <returns>频次数组(8位:256长度,16位:65536长度),失败返回 null | Frequency array, null on failure</returns>
public Task<long[]?> ComputeAsync(byte[] rawData, int width, int height, int bitDepth, CancellationToken ct)
{
// 参数有效性验证 | Parameter validation
if (rawData == null || width <= 0 || height <= 0)
return Task.FromResult<long[]?>(null);
if (bitDepth != 8 && bitDepth != 16)
return Task.FromResult<long[]?>(null);
int expectedLength = bitDepth == 8 ? width * height : width * height * 2;
if (rawData.Length != expectedLength)
return Task.FromResult<long[]?>(null);
// 创建超时令牌 | Create timeout token
var linkedCts = CreateLinkedTimeoutToken(ct);
var linkedToken = linkedCts.Token;
return Task.Run(() =>
{
try
{
if (bitDepth == 8)
{
return ComputeHistogram8Bit(rawData, width, height, linkedToken);
}
else
{
return ComputeHistogram16Bit(rawData, width, height, linkedToken);
}
}
catch (OperationCanceledException)
{
return null;
}
catch
{
return null;
}
finally
{
linkedCts.Dispose();
}
}, linkedToken);
}
/// <summary>
/// 计算 8 位灰度直方图 | Compute 8-bit grayscale histogram
/// </summary>
private static long[]? ComputeHistogram8Bit(byte[] rawData, int width, int height, CancellationToken ct)
{
var histogram = new long[256];
int totalPixels = width * height;
for (int i = 0; i < totalPixels; i++)
{
if (i % 65536 == 0)
ct.ThrowIfCancellationRequested();
histogram[rawData[i]]++;
}
return histogram;
}
/// <summary>
/// 计算 16 位灰度直方图 | Compute 16-bit grayscale histogram
/// </summary>
private static long[]? ComputeHistogram16Bit(byte[] rawData, int width, int height, CancellationToken ct)
{
var histogram = new long[65536];
int totalPixels = width * height;
for (int i = 0; i < totalPixels; i++)
{
if (i % 65536 == 0)
ct.ThrowIfCancellationRequested();
// 小端序读取 16 位值 | Read 16-bit value in little-endian
int offset = i * 2;
ushort value = (ushort)(rawData[offset] | (rawData[offset + 1] << 8));
histogram[value]++;
}
return histogram;
}
/// <summary>
/// 创建带超时的链接取消令牌 | Create linked cancellation token with timeout
/// </summary>
private CancellationTokenSource CreateLinkedTimeoutToken(CancellationToken externalToken)
{
var timeoutCts = new CancellationTokenSource(ComputeTimeoutMs);
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeoutCts.Token);
lock (_lock)
{
_timeoutCts?.Dispose();
_timeoutCts = timeoutCts;
}
return linkedCts;
}
/// <summary>
/// 释放资源 | Dispose resources
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
lock (_lock)
{
_timeoutCts?.Cancel();
_timeoutCts?.Dispose();
_timeoutCts = null;
}
}
}
}
@@ -0,0 +1,70 @@
<UserControl x:Class="XP.Common.Controls.ImageHistogram.ImageHistogramControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
xmlns:local="clr-namespace:XP.Common.Controls.ImageHistogram"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="400">
<UserControl.Resources>
<local:FrequencyLabelConverter x:Key="FreqConverter"/>
</UserControl.Resources>
<Grid>
<!-- 图表控件 | Chart control -->
<telerik:RadCartesianChart x:Name="HistogramChart" Padding="0">
<!-- 禁用 Telerik 自带的无数据提示 | Disable Telerik built-in empty content -->
<telerik:RadCartesianChart.EmptyContent>
<TextBlock/>
</telerik:RadCartesianChart.EmptyContent>
<!-- X 轴:灰度级别(缩小字体,控制刻度数量,K/M 缩写)| X Axis: Gray Level -->
<telerik:RadCartesianChart.HorizontalAxis>
<telerik:LinearAxis x:Name="XAxis"
Minimum="0"
Maximum="255"
MajorStep="64"
FontSize="9">
<telerik:LinearAxis.LabelTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
</DataTemplate>
</telerik:LinearAxis.LabelTemplate>
</telerik:LinearAxis>
</telerik:RadCartesianChart.HorizontalAxis>
<!-- Y 轴:像素频次(K/M 缩写标签)| Y Axis: Pixel Frequency (K/M abbreviation labels) -->
<telerik:RadCartesianChart.VerticalAxis>
<telerik:LinearAxis x:Name="YAxis"
Minimum="0"
Maximum="1"
FontSize="9">
<telerik:LinearAxis.LabelTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
</DataTemplate>
</telerik:LinearAxis.LabelTemplate>
</telerik:LinearAxis>
</telerik:RadCartesianChart.VerticalAxis>
<!-- 面积图系列(适合密集直方图数据)| Area Series (suitable for dense histogram data) -->
<telerik:RadCartesianChart.Series>
<telerik:ScatterAreaSeries x:Name="HistogramBarSeries"
XValueBinding="GrayLevel"
YValueBinding="Frequency"
Fill="#7F2196F3"
Stroke="#FF2196F3"
StrokeThickness="1"/>
</telerik:RadCartesianChart.Series>
</telerik:RadCartesianChart>
<!-- 无数据提示文本(叠加在图表上方)| No data placeholder text (overlaid on chart) -->
<TextBlock x:Name="NoDataPlaceholder"
Text="{loc:Localization Histogram_NoData}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="12"
Foreground="#9E9E9E"
Visibility="Visible"/>
</Grid>
</UserControl>
@@ -0,0 +1,348 @@
using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using Prism.Ioc;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using XP.Common.Logging.Interfaces;
namespace XP.Common.Controls.ImageHistogram
{
/// <summary>
/// 图像灰度直方图通用控件 | Image grayscale histogram control
/// 支持单帧静态图像和高频流式图像输入,使用 Telerik RadChartView 进行可视化渲染
/// </summary>
public partial class ImageHistogramControl : UserControl
{
#region | Dependency Properties
/// <summary>
/// 最大刷新帧率依赖属性 | MaxFrameRate dependency property
/// </summary>
public static readonly DependencyProperty MaxFrameRateProperty =
DependencyProperty.Register(
nameof(MaxFrameRate),
typeof(int),
typeof(ImageHistogramControl),
new PropertyMetadata(15, OnMaxFrameRateChanged, CoerceMaxFrameRate));
/// <summary>
/// 是否使用对数 Y 轴依赖属性 | IsLogarithmic dependency property
/// </summary>
public static readonly DependencyProperty IsLogarithmicProperty =
DependencyProperty.Register(
nameof(IsLogarithmic),
typeof(bool),
typeof(ImageHistogramControl),
new PropertyMetadata(false));
/// <summary>
/// 最大刷新帧率(fps),有效范围 1-60,默认 15 | Max frame rate (fps), valid range 1-60, default 15
/// </summary>
public int MaxFrameRate
{
get => (int)GetValue(MaxFrameRateProperty);
set => SetValue(MaxFrameRateProperty, value);
}
/// <summary>
/// 是否使用对数 Y 轴,默认 false | Whether to use logarithmic Y axis, default false
/// </summary>
public bool IsLogarithmic
{
get => (bool)GetValue(IsLogarithmicProperty);
set => SetValue(IsLogarithmicProperty, value);
}
private static void OnMaxFrameRateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageHistogramControl control)
{
var newValue = (int)e.NewValue;
control._frameThrottler.MaxFrameRate = newValue;
}
}
private static object CoerceMaxFrameRate(DependencyObject d, object baseValue)
{
var value = (int)baseValue;
var clamped = Math.Clamp(value, 1, 60);
if (clamped != value && d is ImageHistogramControl control)
{
control._logger?.Warn(
"MaxFrameRate 值 {Value} 超出有效范围,已钳位为 {Clamped} | MaxFrameRate value {Value} out of range, clamped to {Clamped}",
value, clamped);
}
return clamped;
}
#endregion
#region | Private Fields
private readonly FrameThrottler _frameThrottler;
private readonly HistogramEngine _histogramEngine;
private ChartRenderer? _chartRenderer;
private ILoggerService? _logger;
private CancellationTokenSource? _currentCts;
private readonly object _ctsLock = new();
#endregion
#region | Constructor
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public ImageHistogramControl()
{
InitializeComponent();
// 初始化内部组件 | Initialize internal components
_frameThrottler = new FrameThrottler();
_histogramEngine = new HistogramEngine();
// 尝试解析日志服务 | Try to resolve logger service
try
{
var loggerService = ContainerLocator.Current?.Resolve<ILoggerService>();
_logger = loggerService?.ForModule<ImageHistogramControl>();
}
catch
{
// 日志服务不可用,静默降级 | Logger service unavailable, silent degradation
_logger = null;
}
// 订阅 Loaded 事件初始化 ChartRenderer | Subscribe Loaded event to initialize ChartRenderer
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// 初始化 ChartRenderer | Initialize ChartRenderer
_chartRenderer = new ChartRenderer(HistogramChart, HistogramBarSeries, XAxis);
}
#endregion
#region API | Public API
/// <summary>
/// 传入 ImageSharp 图像对象,计算并显示灰度直方图 | Update histogram from ImageSharp image
/// </summary>
/// <param name="image">ImageSharp 图像对象 | ImageSharp image object</param>
public void UpdateImage(Image<Rgba32> image)
{
try
{
if (image == null)
{
_logger?.Warn("UpdateImage 收到 null 图像,已忽略 | UpdateImage received null image, ignored");
return;
}
SubmitComputation(() => _histogramEngine.ComputeAsync(image, GetOrCreateCancellationToken()));
}
catch (Exception ex)
{
_logger?.Error(ex, "UpdateImage(Image) 异常:{Message} | UpdateImage(Image) error: {Message}", ex.Message);
}
}
/// <summary>
/// 传入原始像素数组,计算并显示灰度直方图 | Update histogram from raw byte array
/// </summary>
/// <param name="rawData">原始像素数据 | Raw pixel data</param>
/// <param name="width">图像宽度 | Image width</param>
/// <param name="height">图像高度 | Image height</param>
/// <param name="bitDepth">位深度(8 或 16| Bit depth (8 or 16)</param>
public void UpdateImage(byte[] rawData, int width, int height, int bitDepth)
{
try
{
// 参数有效性验证 | Parameter validation
if (rawData == null)
{
_logger?.Warn("UpdateImage 收到 null rawData,已忽略 | UpdateImage received null rawData, ignored");
return;
}
if (width <= 0 || height <= 0)
{
_logger?.Warn(
"UpdateImage 参数无效:width={Width}, height={Height} | Invalid params: width={Width}, height={Height}",
width, height);
return;
}
if (bitDepth != 8 && bitDepth != 16)
{
_logger?.Warn(
"UpdateImage 参数无效:bitDepth={BitDepth},仅支持 8 或 16 | Invalid bitDepth={BitDepth}, only 8 or 16 supported",
bitDepth);
return;
}
int expectedLength = bitDepth == 8 ? width * height : width * height * 2;
if (rawData.Length != expectedLength)
{
_logger?.Warn(
"UpdateImage 参数无效:rawData.Length={Length}, 预期={Expected} | Invalid params: rawData.Length={Length}, expected={Expected}",
rawData.Length, expectedLength);
return;
}
SubmitComputation(() => _histogramEngine.ComputeAsync(rawData, width, height, bitDepth, GetOrCreateCancellationToken()));
}
catch (Exception ex)
{
_logger?.Error(ex, "UpdateImage(byte[]) 异常:{Message} | UpdateImage(byte[]) error: {Message}", ex.Message);
}
}
/// <summary>
/// 清空直方图显示,恢复初始空白状态 | Clear histogram display, restore initial blank state
/// </summary>
public void Clear()
{
try
{
// 取消正在执行的后台任务 | Cancel running background task
CancelCurrentComputation();
// 取消帧率限流器中的待处理任务 | Cancel pending tasks in throttler
_frameThrottler.Cancel();
// 清空图表(捕获局部引用避免异步执行时为 null| Clear chart (capture local ref to avoid null during async)
var renderer = _chartRenderer;
if (renderer != null)
{
Dispatcher.InvokeAsync(() =>
{
try
{
renderer.Clear();
// 显示无数据提示 | Show no-data placeholder
NoDataPlaceholder.Visibility = Visibility.Visible;
}
catch
{
// 控件已卸载时忽略 | Ignore if control already unloaded
}
});
}
}
catch (Exception ex)
{
_logger?.Error(ex, "Clear() 异常:{Message} | Clear() error: {Message}", ex.Message);
}
}
#endregion
#region | Private Methods
/// <summary>
/// 通过帧率限流器提交计算任务 | Submit computation through frame throttler
/// </summary>
private void SubmitComputation(Func<System.Threading.Tasks.Task<long[]?>> computeFunc)
{
_frameThrottler.TrySubmit(() =>
{
try
{
var task = computeFunc();
task.ContinueWith(t =>
{
if (t.IsCompletedSuccessfully && t.Result != null)
{
var histogram = t.Result;
var isLog = false;
// 在 UI 线程获取 IsLogarithmic 值并更新图表 | Get IsLogarithmic on UI thread and update chart
Dispatcher.InvokeAsync(() =>
{
try
{
isLog = IsLogarithmic;
_chartRenderer?.UpdateData(histogram, isLog);
// 隐藏无数据提示 | Hide no-data placeholder
NoDataPlaceholder.Visibility = Visibility.Collapsed;
}
catch (Exception ex)
{
_logger?.Error(ex, "图表更新异常:{Message} | Chart update error: {Message}", ex.Message);
}
});
}
else if (t.IsFaulted)
{
_logger?.Error(t.Exception, "直方图计算异常:{Message} | Histogram computation error: {Message}",
t.Exception?.InnerException?.Message ?? "Unknown");
}
}, System.Threading.Tasks.TaskScheduler.Default);
}
catch (Exception ex)
{
_logger?.Error(ex, "提交计算任务异常:{Message} | Submit computation error: {Message}", ex.Message);
}
});
}
/// <summary>
/// 获取或创建取消令牌(取消上一个)| Get or create cancellation token (cancel previous)
/// </summary>
private CancellationToken GetOrCreateCancellationToken()
{
lock (_ctsLock)
{
_currentCts?.Cancel();
_currentCts?.Dispose();
_currentCts = new CancellationTokenSource();
return _currentCts.Token;
}
}
/// <summary>
/// 取消当前计算 | Cancel current computation
/// </summary>
private void CancelCurrentComputation()
{
lock (_ctsLock)
{
_currentCts?.Cancel();
_currentCts?.Dispose();
_currentCts = null;
}
}
/// <summary>
/// Unloaded 事件处理:释放所有资源 | Unloaded event handler: release all resources
/// </summary>
private void OnUnloaded(object sender, RoutedEventArgs e)
{
// 取消所有后台任务 | Cancel all background tasks
CancelCurrentComputation();
// 释放帧率限流器 | Dispose frame throttler
_frameThrottler.Cancel();
_frameThrottler.Dispose();
// 释放计算引擎 | Dispose histogram engine
_histogramEngine.Dispose();
// 清空引用 | Clear references
_chartRenderer = null;
}
#endregion
}
}
@@ -1,6 +1,6 @@
using System;
namespace XP.Common.Controls
namespace XP.Common.Controls.Joystick
{
/// <summary>
/// 虚拟摇杆核心计算逻辑(纯函数,无副作用)| Virtual joystick core calculation logic (pure functions, no side effects)
@@ -1,4 +1,4 @@
namespace XP.Common.Controls
namespace XP.Common.Controls.Joystick
{
/// <summary>
/// 虚拟摇杆轴模式枚举 | Virtual joystick axis mode enumeration
@@ -1,4 +1,4 @@
namespace XP.Common.Controls
namespace XP.Common.Controls.Joystick
{
/// <summary>
/// 鼠标按键类型枚举 | Mouse button type enumeration
@@ -6,7 +6,7 @@ using System.Windows.Media;
using System.Windows.Shapes;
using XP.Common.Logging.Interfaces;
namespace XP.Common.Controls
namespace XP.Common.Controls.Joystick
{
/// <summary>
/// 虚拟摇杆 UserControl,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick UserControl providing mouse drag interaction within a circular area
@@ -1,9 +1,9 @@
<UserControl x:Class="XP.Common.Controls.VirtualJoystick"
<UserControl x:Class="XP.Common.Controls.Joystick.VirtualJoystick"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XP.Common.Controls"
xmlns:local="clr-namespace:XP.Common.Controls.Joystick"
mc:Ignorable="d"
d:DesignWidth="200" d:DesignHeight="200"
Cursor="Hand">
@@ -0,0 +1,46 @@
using System.Configuration;
namespace XP.Common.Database.Configs
{
/// <summary>
/// SQLite 配置加载器,从 App.config 读取数据库相关配置项 | SQLite configuration loader, reads database-related configuration from App.config
/// </summary>
public static class ConfigLoader
{
/// <summary>
/// 配置键前缀 | Configuration key prefix
/// </summary>
private const string KeyPrefix = "Sqlite:";
/// <summary>
/// 从 App.config 加载 SQLite 配置 | Load SQLite configuration from App.config
/// </summary>
/// <returns>SQLite 配置实体,缺失或无效配置项使用默认值 | SQLite configuration entity, uses default values for missing or invalid items</returns>
public static SqliteConfig LoadSqliteConfig()
{
var config = new SqliteConfig();
// 加载数据库文件路径 | Load database file path
var dbPath = ConfigurationManager.AppSettings[KeyPrefix + "DbFilePath"];
if (!string.IsNullOrEmpty(dbPath)) config.DbFilePath = dbPath;
// 加载连接超时时间 | Load connection timeout
var timeout = ConfigurationManager.AppSettings[KeyPrefix + "ConnectionTimeout"];
if (int.TryParse(timeout, out var t) && t > 0) config.ConnectionTimeout = t;
// 加载是否自动创建 | Load create if not exists
var createIfNotExists = ConfigurationManager.AppSettings[KeyPrefix + "CreateIfNotExists"];
if (bool.TryParse(createIfNotExists, out var c)) config.CreateIfNotExists = c;
// 加载是否启用 WAL 模式 | Load enable WAL mode
var enableWal = ConfigurationManager.AppSettings[KeyPrefix + "EnableWalMode"];
if (bool.TryParse(enableWal, out var w)) config.EnableWalMode = w;
// 加载是否开启 SQL 日志 | Load enable SQL logging
var enableSqlLog = ConfigurationManager.AppSettings[KeyPrefix + "EnableSqlLogging"];
if (bool.TryParse(enableSqlLog, out var l)) config.EnableSqlLogging = l;
return config;
}
}
}
@@ -1,46 +1,42 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace XP.Common.Configs
namespace XP.Common.Database.Configs
{
/// <summary>
/// SQLite 配置实体
/// SQLite 配置实体(从 App.config 读取)| SQLite configuration entity (loaded from App.config)
/// </summary>
public class SqliteConfig
{
/// <summary>
/// 数据库文件路径
/// 数据库文件路径 | Database file path
/// </summary>
public string DbFilePath { get; set; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Files", "Data", "XP.db");
/// <summary>
/// 连接超时时间(秒,默认30
/// 连接超时时间(秒,默认30| Connection timeout (seconds, default 30)
/// </summary>
public int ConnectionTimeout { get; set; } = 30;
/// <summary>
/// 数据库不存在时是否自动创建(默认true)
/// 数据库不存在时是否自动创建(默认true)| Whether to auto-create if not exists (default true)
/// </summary>
public bool CreateIfNotExists { get; set; } = true;
/// <summary>
/// 是否启用 WAL 模式(提升并发性能,默认true)
/// 是否启用 WAL 模式(提升并发性能,默认true)| Whether to enable WAL mode (default true)
/// </summary>
public bool EnableWalMode { get; set; } = true;
/// <summary>
/// 是否开启日志记录(记录所有SQL操作,默认false)
/// 是否开启日志记录(记录所有SQL操作,默认false)| Whether to enable SQL logging (default false)
/// </summary>
public bool EnableSqlLogging { get; set; } = false;
/// <summary>
/// 获取SQLite连接字符串
/// 获取 SQLite 连接字符串 | Get SQLite connection string
/// </summary>
public string GetConnectionString()
{
@@ -7,7 +7,7 @@ using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using XP.Common.Configs;
using XP.Common.Database.Configs;
using XP.Common.Database.Interfaces;
using XP.Common.Database.Models;
using XP.Common.Helpers;
+184
View File
@@ -0,0 +1,184 @@
# 授权服务使用指南 | License Service Usage Guide
## 概述 | Overview
XplorePlane 通过 `XP.Common.License` 命名空间下的 `ILicenseService` 提供统一的授权管理。底层基于海克斯康 CLMSComputational License Management SystemSDK,使用 `MORCODE.dll` 进行许可证校验。
## 产品授权信息 | Product License Information
| 项目 | 值 |
| --- | --- |
| CLMS 模块 IDModule ID | `4` |
| 零件号(Part Number | `LS950-0071-5-1` |
`Module ID` 是 CLMS 在 SDK 中标识本产品的唯一编号,调用 `CLM_ModuleIsLicensed` 时必须传入此值;`Part Number` 是产品在 CLMS 许可发行系统中的物料编号,用于申请、签发、续期许可证。
## 授权模式 | License Modes
`LicenseMode` 枚举定义见 `XP.Common.License.Enums.LicenseMode`
| 模式 | 枚举值 | 数值 | 说明 |
| --- | --- | --- | --- |
| CLMS 正式授权 | `Clms` | `0` | 通过 CLMS SDK 进行完整授权校验,包含登录、许可范围、模块、SMA、到期日期等检查 |
| 临时测试模式 | `TemporaryTest` | `885` | 不调用 CLMS SDK,直接放行 15 分钟,用于研发/测试场景 |
## 配置项 | Configuration Items
授权配置位于主应用 `App.config` 中,键前缀为 `License:`,对应 `XP.Common.License.Configs.LicenseConfig`
```xml
<appSettings>
<!-- 授权配置 | License configuration -->
<add key="License:LicenseMode" value="0" /> <!-- 授权模式:0=CLMS 正式授权,885=临时测试模式 -->
<add key="License:ModuleId" value="4" /> <!-- 模块 IDXplorePlane 固定为 4 -->
<add key="License:UseSma" value="false" /> <!-- 是否启用 SMA 检查 -->
<add key="License:LicenseState" value="20" /> <!-- 上次授权状态:10=成功,20=失败(运行时由 LicenseService 自动写回)-->
</appSettings>
```
| 键 | 类型 | 默认值 | 有效值 | 说明 |
| --- | --- | --- | --- | --- |
| `License:LicenseMode` | int | `0` | `0``885` | 授权模式 |
| `License:ModuleId` | ushort | `4` | `1` ~ `65535` | CLMS 模块 ID |
| `License:UseSma` | bool | `false` | `true``false` | 是否启用 SMA(软件维护协议)校验 |
| `License:LicenseState` | int | `20` | `10``20` | 上次运行时的授权结果,由服务自动维护 |
> 配置中无效或缺失的键会回退到默认值,详见 `XP.Common.License.Configs.ConfigLoader`
## 正式授权流程 | Formal Authorization Flow
`LicenseMode = 0``Clms`)时,`LicenseService.CheckAuthorization()` 依次执行以下步骤:
1. **系统时间检查**(可选):调用 `CLM_CheckSystemTime`,老版本 SDK 缺失此入口点时跳过。
2. **登录验证**:调用 `CLM_Login`(核心步骤,必须存在)。
3. **许可范围检查**:调用 `CLM_Login_Scope`(核心步骤)。
4. **SMA 验证**(可选):仅当 `License:UseSma = true` 时执行。比较 SMA 年份/季度与当前软件主版本号/次版本号,季度不匹配会判定失败。
5. **浮动许可信息**(可选):通过 `CLM_GetIP` 获取 IP 与端口。
6. **错误信息读取**(可选):通过 `CLM_GetError` 拉取 SDK 端错误描述。
7. **模块授权检查**:调用 `CLM_ModuleIsLicensed`,传入 `Module ID = 4`
8. **到期日期获取**:调用 `CLM_GetWarrantyExpiration`,剩余 ≤ 30 天时记录警告。
任意核心步骤失败即视为授权失败,写回 `License:LicenseState = 20`,主应用弹窗提示并退出。
## 临时测试模式 | Temporary Test Mode
`LicenseMode = 885``TemporaryTest`)用于跳过 CLMS 校验,便于离线开发和功能演示。
### 行为 | Behavior
- 不加载 `MORCODE.dll`,不调用任何 CLMS API,授权直接通过。
- 启动后立刻开启 15 分钟(900 秒)倒计时,到期触发 `TestModeTimeout` 事件,主应用应执行优雅关闭。
- 倒计时途中分别在剩余 5 分钟、1 分钟时触发 `TestModeWarning5Min``TestModeWarning1Min` 事件用于提醒。
- 上述三个事件只在临时测试模式下被触发,正式授权(`Clms`)下不会创建计时器。
### 启用方式 | How to Enable
修改主应用 `App.config`
```xml
<add key="License:LicenseMode" value="885" />
```
启动后将看到提示:「当前为临时测试模式,软件将在 15 分钟后自动关闭」。
> **注意**:临时测试模式仅用于研发与内部测试场景,**禁止用于生产或交付**。发布前请务必将 `License:LicenseMode` 还原为 `0`
## 使用示例 | Usage Examples
### 注入并校验 | Inject and Verify
```csharp
using XP.Common.License.Interfaces;
using XP.Common.License.Enums;
public class StartupChecker
{
private readonly ILicenseService _licenseService;
private readonly ILoggerService _logger;
public StartupChecker(ILicenseService licenseService, ILoggerService logger)
{
_licenseService = licenseService ?? throw new ArgumentNullException(nameof(licenseService));
_logger = logger?.ForModule<StartupChecker>() ?? throw new ArgumentNullException(nameof(logger));
}
public bool Run()
{
var result = _licenseService.CheckAuthorization();
if (!result.IsAuthorized)
{
_logger.Error(null, "授权失败:{Message} | License failed: {Message}", result.Message);
return false;
}
// 仅在临时测试模式下订阅倒计时事件 | Subscribe countdown events only in test mode
if (_licenseService.LicenseMode == LicenseMode.TemporaryTest)
{
_licenseService.TestModeWarning5Min += (s, e) => _logger.Warn("临时测试模式剩余 5 分钟");
_licenseService.TestModeWarning1Min += (s, e) => _logger.Warn("临时测试模式剩余 1 分钟");
_licenseService.TestModeTimeout += (s, e) => Application.Current.Shutdown();
}
return true;
}
}
```
### 检查特定模块授权 | Check Module Authorization
```csharp
const ushort XplorePlaneModuleId = 4;
if (!_licenseService.IsModuleLicensed(XplorePlaneModuleId))
{
_logger.Warn("模块 {ModuleId} 未授权 | Module {ModuleId} not licensed", XplorePlaneModuleId);
}
```
### 读取授权信息 | Read License Information
```csharp
DateTime? expiration = _licenseService.GetExpirationDate(); // 授权到期日期
DateTime? sma = _licenseService.GetSmaDate(); // SMA 到期日期
int remaining = _licenseService.GetRemainingTestTime(); // 临时测试剩余秒数;非测试模式返回 -1
```
## 接口与事件 | Interface & Events
`ILicenseService` 提供以下成员(详见 `XP.Common.License.Interfaces.ILicenseService`):
- `LicenseCheckResult CheckAuthorization()`:执行完整授权校验。
- `bool IsAuthorized`:当前会话是否已授权。
- `LicenseMode LicenseMode`:当前授权模式。
- `DateTime? GetExpirationDate()`:授权到期日期。
- `DateTime? GetSmaDate()`SMA 到期日期。
- `bool IsModuleLicensed(ushort moduleId)`:检查指定模块是否被授权。
- `int GetRemainingTestTime()`:临时测试模式剩余秒数。
- 事件:`TestModeWarning5Min``TestModeWarning1Min``TestModeTimeout`
## 常见问题 | FAQ
### 1. 启动时提示 `MORCODE.dll 加载失败` | `Failed to load MORCODE.dll`
确认 `MORCODE.dll` 已随 `ReleaseFiles` 一同部署,并位于主程序同级目录。研发阶段可临时切换到临时测试模式绕过。
### 2. 提示「模块号码 4 不可用」| `Module 4 unavailable`
CLMS 服务器签发的许可证未包含 `Module ID = 4`(零件号 `LS950-0071-5-1`)。请联系海克斯康许可团队确认许可范围。
### 3. SMA 校验失败 | SMA validation failed
SMA 年份必须 ≥ 软件主版本号;同年时 SMA 季度必须 ≥ 软件次版本号。若需临时绕过,可将 `License:UseSma` 设为 `false`,但发布版本仍应保持 SMA 校验启用。
### 4. 授权成功后是否还有倒计时?| Will the countdown still trigger after a successful formal authorization?
不会。倒计时仅在 `LicenseMode = TemporaryTest` 时启动,`Clms` 正式授权下三个事件永远不会被触发。
## 相关文件 | Related Files
- `XP.Common/License/Interfaces/ILicenseService.cs` — 服务接口
- `XP.Common/License/Implementations/LicenseService.cs` — 服务实现
- `XP.Common/License/Configs/LicenseConfig.cs` — 配置实体
- `XP.Common/License/Configs/ConfigLoader.cs` — 配置加载器
- `XP.Common/License/Enums/LicenseMode.cs` — 授权模式枚举
- `XP.Common/License/Enums/LicenseState.cs` — 授权状态枚举
- `XP.Common/License/Native/NativeMethods.cs` — CLMS SDK P/Invoke 封装
- `XplorePlane/App.xaml.cs` — 主应用启动时调用 `PerformLicenseCheck()` 的入口
+46
View File
@@ -0,0 +1,46 @@
using System.Configuration;
namespace XP.Common.Dump.Configs
{
/// <summary>
/// Dump 配置加载器,从 App.config 读取 Dump 相关配置项 | Dump configuration loader, reads dump-related configuration from App.config
/// </summary>
public static class ConfigLoader
{
/// <summary>
/// 配置键前缀 | Configuration key prefix
/// </summary>
private const string KeyPrefix = "Dump:";
/// <summary>
/// 从 App.config 加载 Dump 配置 | Load Dump configuration from App.config
/// </summary>
/// <returns>Dump 配置实体,缺失或无效配置项使用默认值 | Dump configuration entity, uses default values for missing or invalid items</returns>
public static DumpConfig LoadDumpConfig()
{
var config = new DumpConfig();
// 加载存储路径 | Load storage path
var storagePath = ConfigurationManager.AppSettings[KeyPrefix + "StoragePath"];
if (!string.IsNullOrEmpty(storagePath)) config.StoragePath = storagePath;
// 加载是否启用定时触发 | Load enable scheduled dump
var enableScheduled = ConfigurationManager.AppSettings[KeyPrefix + "EnableScheduledDump"];
if (bool.TryParse(enableScheduled, out var enabled)) config.EnableScheduledDump = enabled;
// 加载定时触发间隔 | Load scheduled interval
var interval = ConfigurationManager.AppSettings[KeyPrefix + "ScheduledIntervalMinutes"];
if (int.TryParse(interval, out var min) && min > 0) config.ScheduledIntervalMinutes = min;
// 加载 Mini Dump 文件大小上限 | Load Mini Dump size limit
var sizeLimit = ConfigurationManager.AppSettings[KeyPrefix + "MiniDumpSizeLimitMB"];
if (long.TryParse(sizeLimit, out var size) && size > 0) config.MiniDumpSizeLimitMB = size;
// 加载文件保留天数 | Load retention days
var retentionDays = ConfigurationManager.AppSettings[KeyPrefix + "RetentionDays"];
if (int.TryParse(retentionDays, out var days) && days > 0) config.RetentionDays = days;
return config;
}
}
}
-91
View File
@@ -1,91 +0,0 @@
using System.Configuration;
using XP.Common.Configs;
using XP.Common.Dump.Configs;
namespace XP.Common.Helpers
{
/// <summary>
/// 通用配置加载工具(读取App.config)
/// </summary>
public static class ConfigLoader
{
/// <summary>
/// 加载Serilog配置
/// </summary>
public static SerilogConfig LoadSerilogConfig()
{
var config = new SerilogConfig();
var logPath = ConfigurationManager.AppSettings["Serilog:LogPath"];
if (!string.IsNullOrEmpty(logPath)) config.LogPath = logPath;
var minLevel = ConfigurationManager.AppSettings["Serilog:MinimumLevel"];
if (!string.IsNullOrEmpty(minLevel)) config.MinimumLevel = minLevel;
var enableConsole = ConfigurationManager.AppSettings["Serilog:EnableConsole"];
if (bool.TryParse(enableConsole, out var console)) config.EnableConsole = console;
var rollingInterval = ConfigurationManager.AppSettings["Serilog:RollingInterval"];
if (!string.IsNullOrEmpty(rollingInterval)) config.RollingInterval = rollingInterval;
var fileSize = ConfigurationManager.AppSettings["Serilog:FileSizeLimitMB"];
if (long.TryParse(fileSize, out var size)) config.FileSizeLimitMB = size;
var retainCount = ConfigurationManager.AppSettings["Serilog:RetainedFileCountLimit"];
if (int.TryParse(retainCount, out var count)) config.RetainedFileCountLimit = count;
return config;
}
/// <summary>
/// 加载SQLite配置
/// </summary>
public static SqliteConfig LoadSqliteConfig()
{
var config = new SqliteConfig();
var dbPath = ConfigurationManager.AppSettings["Sqlite:DbFilePath"];
if (!string.IsNullOrEmpty(dbPath)) config.DbFilePath = dbPath;
var timeout = ConfigurationManager.AppSettings["Sqlite:ConnectionTimeout"];
if (int.TryParse(timeout, out var t)) config.ConnectionTimeout = t;
var createIfNotExists = ConfigurationManager.AppSettings["Sqlite:CreateIfNotExists"];
if (bool.TryParse(createIfNotExists, out var c)) config.CreateIfNotExists = c;
var enableWal = ConfigurationManager.AppSettings["Sqlite:EnableWalMode"];
if (bool.TryParse(enableWal, out var w)) config.EnableWalMode = w;
var enableSqlLog = ConfigurationManager.AppSettings["Sqlite:EnableSqlLogging"];
if (bool.TryParse(enableSqlLog, out var l)) config.EnableSqlLogging = l;
return config;
}
/// <summary>
/// 加载 Dump 配置 | Load Dump configuration
/// </summary>
public static DumpConfig LoadDumpConfig()
{
var config = new DumpConfig();
var storagePath = ConfigurationManager.AppSettings["Dump:StoragePath"];
if (!string.IsNullOrEmpty(storagePath)) config.StoragePath = storagePath;
var enableScheduled = ConfigurationManager.AppSettings["Dump:EnableScheduledDump"];
if (bool.TryParse(enableScheduled, out var enabled)) config.EnableScheduledDump = enabled;
var interval = ConfigurationManager.AppSettings["Dump:ScheduledIntervalMinutes"];
if (int.TryParse(interval, out var min)) config.ScheduledIntervalMinutes = min;
var sizeLimit = ConfigurationManager.AppSettings["Dump:MiniDumpSizeLimitMB"];
if (long.TryParse(sizeLimit, out var size)) config.MiniDumpSizeLimitMB = size;
var retentionDays = ConfigurationManager.AppSettings["Dump:RetentionDays"];
if (int.TryParse(retentionDays, out var days)) config.RetentionDays = days;
return config;
}
}
}
+102
View File
@@ -0,0 +1,102 @@
using System.Configuration;
namespace XP.Common.License.Configs
{
/// <summary>
/// 授权配置加载器,从 App.config 读取授权相关配置项 | License configuration loader, reads license-related configuration from App.config
/// </summary>
public static class ConfigLoader
{
/// <summary>
/// 配置键前缀 | Configuration key prefix
/// </summary>
private const string KeyPrefix = "License:";
/// <summary>
/// LicenseMode 有效值集合 | Valid values for LicenseMode
/// </summary>
private static readonly int[] ValidLicenseModes = { 0, 885 };
/// <summary>
/// LicenseState 有效值集合 | Valid values for LicenseState
/// </summary>
private static readonly int[] ValidLicenseStates = { 10, 20 };
/// <summary>
/// 从 App.config 加载授权配置 | Load license configuration from App.config
/// </summary>
/// <returns>授权配置实体,缺失或无效配置项使用默认值 | License configuration entity, uses default values for missing or invalid items</returns>
public static LicenseConfig LoadLicenseConfig()
{
var config = new LicenseConfig();
// 加载 LicenseMode | Load LicenseMode
var licenseModeStr = ConfigurationManager.AppSettings[KeyPrefix + "LicenseMode"];
if (int.TryParse(licenseModeStr, out var licenseMode) && IsValidLicenseMode(licenseMode))
{
config.LicenseMode = licenseMode;
}
// 加载 ModuleId | Load ModuleId
var moduleIdStr = ConfigurationManager.AppSettings[KeyPrefix + "ModuleId"];
if (ushort.TryParse(moduleIdStr, out var moduleId) && IsValidModuleId(moduleId))
{
config.ModuleId = moduleId;
}
// 加载 UseSma | Load UseSma
var useSmaStr = ConfigurationManager.AppSettings[KeyPrefix + "UseSma"];
if (bool.TryParse(useSmaStr, out var useSma))
{
config.UseSma = useSma;
}
// 加载 LicenseState | Load LicenseState
var licenseStateStr = ConfigurationManager.AppSettings[KeyPrefix + "LicenseState"];
if (int.TryParse(licenseStateStr, out var licenseState) && IsValidLicenseState(licenseState))
{
config.LicenseState = licenseState;
}
return config;
}
/// <summary>
/// 验证 LicenseMode 值是否有效 | Validate whether LicenseMode value is valid
/// </summary>
/// <param name="value">待验证的值 | Value to validate</param>
/// <returns>true 表示有效,false 表示无效 | true if valid, false if invalid</returns>
private static bool IsValidLicenseMode(int value)
{
foreach (var valid in ValidLicenseModes)
{
if (value == valid) return true;
}
return false;
}
/// <summary>
/// 验证 ModuleId 值是否在有效范围内 | Validate whether ModuleId value is within valid range
/// </summary>
/// <param name="value">待验证的值 | Value to validate</param>
/// <returns>true 表示有效,false 表示无效 | true if valid, false if invalid</returns>
private static bool IsValidModuleId(ushort value)
{
return value >= 1 && value <= 65535;
}
/// <summary>
/// 验证 LicenseState 值是否有效 | Validate whether LicenseState value is valid
/// </summary>
/// <param name="value">待验证的值 | Value to validate</param>
/// <returns>true 表示有效,false 表示无效 | true if valid, false if invalid</returns>
private static bool IsValidLicenseState(int value)
{
foreach (var valid in ValidLicenseStates)
{
if (value == valid) return true;
}
return false;
}
}
}
@@ -0,0 +1,28 @@
namespace XP.Common.License.Configs
{
/// <summary>
/// 授权配置实体 | License configuration entity
/// </summary>
public class LicenseConfig
{
/// <summary>
/// 授权模式 | License mode
/// </summary>
public int LicenseMode { get; set; } = 0;
/// <summary>
/// 模块ID | Module ID
/// </summary>
public ushort ModuleId { get; set; } = 4;
/// <summary>
/// 是否使用SMA | Whether to use SMA
/// </summary>
public bool UseSma { get; set; } = false;
/// <summary>
/// 授权状态 | License state
/// </summary>
public int LicenseState { get; set; } = 20;
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace XP.Common.License.Enums;
/// <summary>
/// 授权模式枚举 | License mode enumeration
/// </summary>
public enum LicenseMode : int
{
/// <summary>
/// CLMS 正式授权 | CLMS formal authorization
/// </summary>
Clms = 0,
/// <summary>
/// 临时测试模式(15分钟)| Temporary test mode (15 minutes)
/// </summary>
TemporaryTest = 885
}
+17
View File
@@ -0,0 +1,17 @@
namespace XP.Common.License.Enums;
/// <summary>
/// 授权状态枚举 | License state enumeration
/// </summary>
public enum LicenseState : int
{
/// <summary>
/// 授权成功 | Authorization successful
/// </summary>
Success = 10,
/// <summary>
/// 授权失败 | Authorization failed
/// </summary>
Fail = 20
}

Some files were not shown because too many files have changed in this diff Show More