【C#】WPF基于Halcon 的HWindowControlWPF 控件实现图像缩放、移动

基于Halcon的HWindowControlWPF控件实现图像缩放与平移,核心在于理解其独特的ImagePart机制------这与常规WPF的RenderTransform体系完全不同。

一、核心机制:ImagePart控制显示区域

Halcon控件的图像缩放不是通过变换矩阵实现的,而是通过控制ImagePart(图像显示区域)来完成。
ImagePart的本质

ImagePart定义了在固定尺寸的控件窗口中要显示图像的哪个矩形区域,格式为 [Row1, Column1, Row2, Column2],即图像坐标系中的左上角和右下角坐标。

  • 放大效果:缩小ImagePart的范围。例如将显示区域从整幅图像 (0, 0, 800, 600) 缩小到 (0, 0, 400, 300),相当于用半幅图像填充同样大小的控件窗口,实现2倍放大。
  • 平移效果:修改ImagePart的起始坐标。例如将左上角从 (0, 0) 移动到 (100, 100),图像内容就会向相反方向平移。
  • 缩放中心:以鼠标当前位置为中心进行缩放时,需要保持鼠标所指像素在缩放前后的相对位置不变。这通过计算缩放前后的坐标偏移量,调整ImagePart的起始和终止坐标来实现。

关键优势

Halcon控件的鼠标事件(如HMouseDown、HMouseMove、HMouseUp、HMouseWheel)返回的是图像坐标系中的坐标,而非屏幕坐标。这意味着您无需进行繁琐的坐标转换,直接获取的就是图像上的像素位置,极大简化了交互逻辑。

二、交互事件体系

Halcon控件提供专门的鼠标事件,与WPF原生事件不同:

  • 拖拽平移的实现逻辑
    当用户按下鼠标左键并移动时,记录起始图像坐标,在移动过程中计算坐标差值,将差值应用到ImagePart的四个边界上,实现图像跟随鼠标移动的效果。由于事件直接返回图像坐标,计算出的差值就是图像像素级别的移动距离,无需考虑缩放比例因子。
  • 滚轮缩放的实现逻辑
    滚轮事件中,首先获取鼠标当前在图像上的坐标作为缩放中心点。然后计算缩放后的ImagePart范围:新宽度 = 原宽度 / 缩放因子,新高度 = 原高度 / 缩放因子。接着调整ImagePart的左上角和右下角坐标,确保鼠标指向的像素在缩放前后保持在屏幕上的相对位置不变。

三、关键边界条件处理

  • 图像尺寸限制
    Halcon对图像尺寸有严格限制(普通版最大支持32K×32K)。当进行无限缩小时,ImagePart的范围可能超出此限制,导致程序崩溃。因此必须在缩放逻辑中增加边界检查:当计算出的ImagePart尺寸超过阈值时,限制缩放比例或拒绝本次缩放操作。
  • 最小显示范围
    当图像缩放到极小时,ImagePart的宽度和高度可能小于1个像素,导致显示异常。建议设置最小显示范围(例如10×10像素),防止用户过度缩小。
  • 图像边界约束
    平移操作不应使ImagePart超出图像原始边界。当用户拖拽到图像边缘时,应阻止进一步向该方向的平移,或采用弹性边界效果(允许短暂超出后回弹)。

四、性能优化策略

  • 双缓冲机制
    Halcon控件默认绘制到前台窗口,频繁刷新(如快速缩放或拖拽)可能导致闪烁。建议启用双缓冲:使用离屏窗口(Offscreen Window)先完成所有绘制操作,再一次性将结果渲染到前台控件中。这在显示复杂图形叠加(如检测框、测量线)时尤为重要。
  • 图像预加载与缓存
    对于大幅面图像,考虑将图像数据预加载到Halcon的图像对象中,避免在交互过程中频繁从磁盘读取。Halcon的图像对象存储在内存中,访问速度远快于文件I/O。
  • 显示区域优化
    当图像分辨率远高于屏幕分辨率时,可以只加载和显示当前ImagePart对应的图像区域(类似地图切片技术),而非整幅图像。这在处理工业相机采集的超高分辨率图像(如千万像素级)时能显著提升性能。

五、代码实现

5.1 Halcon代码实现

csharp 复制代码
//
//  File generated by HDevelop for HALCON/DOTNET (C#) Version 12.0
//
//  This file is intended to be used with the HDevelopTemplate or
//  HDevelopTemplateWPF projects located under %HALCONEXAMPLES%\c#

using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using HalconDotNet;

public partial class HalconImageHandle
{
    HTuple? hv_ExpDefaultWinHandle, RowDown, ColDown;
    HObject? ho_Image;
    bool isMouseDown = false;

    /// <summary>
    /// 鼠标按下
    /// </summary>
    public void MouseDown()
    {
        try
        {
            HTuple Row, Column, Button;
            //返回鼠标当前按下点的图像坐标
            HOperatorSet.GetMposition(hv_ExpDefaultWinHandle, out Row, out Column, out Button);
            RowDown = Row;    //鼠标按下时的行坐标
            ColDown = Column; //鼠标按下时的列坐标
            if (Button.I == 1)//左键嗯下值为1
                isMouseDown = true;
        }
        catch (Exception)
        {

            throw;
        }
    }

    /// <summary>
    /// 鼠标提起
    /// </summary>
    public void MouseUp()
    {
        try
        {
            if (isMouseDown)
            {
                isMouseDown = false;
            }
        }
        catch (Exception)
        {
        }
    }

    /// <summary>
    /// 移动
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    public void imagetrans()
    {
        try
        {
            if (!isMouseDown)
            {
                return;
            }
            HTuple Row, Column, Button;
            //获取当前鼠标的坐标值
            HOperatorSet.GetMposition(hv_ExpDefaultWinHandle, out Row, out Column, out Button);
            double RowMove = Row - RowDown;   //鼠标弹起时的行坐标减去按下时的行坐标,得到行坐标的移动值
            double ColMove = Column - ColDown;//鼠标弹起时的列坐标减去按下时的列坐标,得到列坐标的移动值
            HTuple row1, col1, row2, col2;

            //得到当前的窗口坐标
            HOperatorSet.GetPart(hv_ExpDefaultWinHandle, out row1, out col1, out row2, out col2);
            //移动后的左上角和右下角坐标,这里可能有些不好理解。以左上角原点为参考点
            HOperatorSet.SetPart(hv_ExpDefaultWinHandle, row1 - RowMove, col1 - ColMove, row2 - RowMove, col2 - ColMove);

            HOperatorSet.ClearWindow(hv_ExpDefaultWinHandle);
            HOperatorSet.DispObj(ho_Image, hv_ExpDefaultWinHandle);
        }
        catch (Exception)
        {

            throw;
        }
    }

    /// <summary>
    /// 缩放
    /// </summary>
    /// <param name="Zoom"></param>
    public void imagescale(HTuple Zoom)
    {
        try
        {
            //放大倍数,当前鼠标选择的图像点坐标Row, Col,按下鼠标的左键还是右键:o-没按下,1-左键,2-中键,4-右键
            HTuple Row, Col, Button;
            HTuple RowLeftUpper, ColumnLeftUpper, RowRightLower, ColumnRightLower, Ht, Wt, ImagePartRowLeftUp, ImagePartColLeftUp, ImagePartRowRightLow, ImagePartColRightLow;
            //返回输出窗口中鼠标指针和鼠标按钮所按下的像素精确图像坐标,输出当前鼠标指针点的图像坐标以及按下的是鼠标左键还是右键,0是鼠标左键
            HOperatorSet.GetMposition(hv_ExpDefaultWinHandle, out Row, out Col, out Button);
            //Get part返回窗口中显示的图像部分的左上角和右下角

            //得到当前的窗口坐标,Row0:图像部分左上角的行索引,Column0:图像部分左上角的列索引,Row00:图像部分右下角的行索引,Column00:图像部分右下角的列索引
            HOperatorSet.GetPart(hv_ExpDefaultWinHandle, out RowLeftUpper, out ColumnLeftUpper, out RowRightLower, out ColumnRightLower);
            //显示的部分图像的高
            Ht = RowRightLower - RowLeftUpper;
            //显示的部分图像的宽
            Wt = ColumnRightLower - ColumnLeftUpper;
            //普通版halcon能处理的图像最大尺寸是32K*32K。如果无限缩小原图像,导致显示的图像超出限制,则会造成程序崩溃
            if (Ht * Wt < 32000 * 32000 || Zoom == 1.5)
            {
                //显示的放大或者缩小部分图像的左上角和右下角坐标
                ImagePartRowLeftUp = (RowLeftUpper + ((1 - (1.0 / Zoom)) * (Row - RowLeftUpper)));
                ImagePartColLeftUp = (ColumnLeftUpper + ((1 - (1.0 / Zoom)) * (Col - ColumnLeftUpper)));
                ImagePartRowRightLow = ImagePartRowLeftUp + (Ht / Zoom);
                ImagePartColRightLow = ImagePartColLeftUp + (Wt / Zoom);
                //设置部分显示图像
                HOperatorSet.SetPart(hv_ExpDefaultWinHandle, ImagePartRowLeftUp, ImagePartColLeftUp, ImagePartRowRightLow, ImagePartColRightLow);
                HOperatorSet.ClearWindow(hv_ExpDefaultWinHandle);
                HOperatorSet.DispObj(ho_Image, hv_ExpDefaultWinHandle);
            }
        }
        catch (Exception)
        {

            throw;
        }
    }

    // Main procedure 
    private void action(String filePath)
    {
        HOperatorSet.GenEmptyObj(out ho_Image);
        HOperatorSet.ReadImage(out ho_Image, filePath);
        HOperatorSet.DispObj(ho_Image, hv_ExpDefaultWinHandle);
    }

    public void InitHalcon()
    {
        // Default settings used in HDevelop 
        HOperatorSet.SetSystem("width", 512);
        HOperatorSet.SetSystem("height", 512);
    }

    public void RunHalcon(HTuple Window, String filePath)
    {
        hv_ExpDefaultWinHandle = Window;
        action(filePath);
    }

    public void Dispose()
    {
        ho_Image?.Dispose();
    }
}

5.2 WPF实现

  • 控件引用
csharp 复制代码
<halcon:HWindowControlWPF
    x:Name="HW_Gerber"
    Grid.Row="1"
    Grid.Column="0"
    Grid.ColumnSpan="2"
    Margin="1,0,1,1"
    HMouseDown="HW_Gerber_HMouseDown"
    HMouseMove="HW_Gerber_HMouseMove"
    HMouseUp="HW_Gerber_HMouseUp"
    HMouseWheel="HW_Gerber_HMouseWheel" />
  
  • 后台实现
csharp 复制代码
private void HW_Gerber_HMouseDown(object sender, HMouseEventArgsWPF e)
{
    try
    {
        boardTestImageViewModel?.MouseDown();
    }
    catch (Exception)
    {
    }
}

private void HW_Gerber_HMouseMove(object sender, HMouseEventArgsWPF e)
{
    try
    {
        boardTestImageViewModel?.MouseMove();
    }
    catch (Exception)
    {
    }
}

private void HW_Gerber_HMouseWheel(object sender, HMouseEventArgsWPF e)
{
    try
    {
        HTuple Zoom;
        //鼠标向上滚动表示放大
        if (e.Delta > 0)
        {
            Zoom = 1.2;
        }
        //向下滚动缩小
        else
        {
            Zoom = 0.8;
        }
        boardTestImageViewModel?.MouseWheel(Zoom);

    }
    catch (Exception)
    {
    }
}

private void HW_Gerber_HMouseUp(object sender, HMouseEventArgsWPF e)
{
    boardTestImageViewModel?.MouseUp();
}
  • ViewModel实现
csharp 复制代码
HalconImageHandle imageHandle = new HalconImageHandle();
/// <summary>
 /// 加载图像
 /// </summary>
 /// <param name="fileName"></param>
 private void LoadImage(string fileName)
 {
     Task.Factory.StartNew(() =>
     {
         Thread.Sleep(1000);
         string filePath = Path.Combine(DirPath, fileName);
         imageHandle.RunHalcon(page?.HW_Gerber.HalconWindow, filePath);
     });
 }

 /// <summary>
 /// 图像缩放
 /// </summary>
 /// <param name="Zoom"></param>
 public void MouseWheel(HTuple Zoom)
 {
     imageHandle.imagescale(Zoom);
 }
   
 /// <summary>
 /// 鼠标移动
 /// </summary>
 public void MouseMove()
 {
     imageHandle.imagetrans();
 }

 /// <summary>
 /// 鼠标按下
 /// </summary>
 public void MouseDown()
 {
     imageHandle.MouseDown();
 }

 /// <summary>
 /// 鼠标放开
 /// </summary>
 public void MouseUp()
 {
     imageHandle.MouseUp();
 }

六、典型应用场景扩展

  • 图像标注与测量
    由于Halcon事件直接返回图像坐标,实现标注功能非常直接:用户在图像上点击,获取的坐标直接对应原始图像像素,无需坐标逆变换。可以基于此实现长度测量、角度测量、缺陷标记等功能。
  • 多窗口同步
    在多视图显示场景中(如同时显示原图、放大细节图、ROI区域图),可以通过共享ImagePart状态实现窗口联动:当主窗口缩放或平移时,其他窗口同步更新各自的ImagePart,保持显示内容的空间一致性。
  • 与Halcon算子集成
    缩放和平移操作可以与Halcon的图像处理算子无缝结合。例如,用户在放大后的图像上框选ROI区域,可以直接将ImagePart坐标作为Halcon算子(如reduce_domain、crop_rectangle1)的参数,进行后续的图像处理和分析。

七、总结

Halcon的HWindowControlWPF通过ImagePart机制提供了一种与图像坐标系紧密集成的缩放平移方案。相比WPF原生的Transform体系,其核心优势在于:

  • 坐标直接对应:鼠标事件返回图像坐标,无需复杂的坐标转换矩阵计算
  • 精度保障:所有操作基于原始图像像素坐标,不存在浮点精度累积误差
  • 生态整合:与Halcon的图像处理算子无缝衔接,适合机器视觉应用场景

开发时需注意图像尺寸限制和边界条件处理,建议配合双缓冲机制提升交互流畅度。对于需要亚像素级精度的应用场景,这种基于原始坐标的操作方式能有效避免因坐标变换引入的精度损失。

相关推荐
ComputerInBook1 小时前
C++ 中的 lambda 表达式
开发语言·c++·lambda表达式·匿名函数
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_52:(深入 XPathExpression 接口)
开发语言·前端·javascript·ui·html·音视频
雪豹阿伟2 小时前
2.C# —— 结构体、类型转换与运算符
c#·上位机
yuanpan2 小时前
Python + Selenium 浏览器自动化测试与网页自动登录
开发语言·python·selenium
Wy_编程2 小时前
Go语言中的指针
开发语言·后端·golang
不想写代码的星星2 小时前
C++协程从入门到放弃?不,是从入门到手搓调度器
开发语言·c++
lolo大魔王2 小时前
Go语言数据库操作之GORM框架从入门到生产实战(完整版)
开发语言·数据库·golang
cndes2 小时前
Pycharm的虚拟环境设置问题
开发语言·python
河阿里3 小时前
Java包装类(Wrapper):自动装箱拆箱机制与类型转换的那些坑
java·开发语言