【Halcon 】用 Halcon 实现涂抹:Region、仿射变换与 WPF 交互

用 Halcon 实现涂抹:Region、仿射变换与 WPF 交互

本文说明在 C# + Halcon + WPF 下,如何用 Region(区域) 实现类似「画笔涂抹」的模板制作交互,并附本仓库 实际源码摘录 (路径相对于仓库根目录 视觉定位)。


目录

  1. [为何用 Region 而不是像素画板](#为何用 Region 而不是像素画板)
  2. 数据结构与鼠标状态
  3. [事件订阅:从 ROI 窗口到 ViewModel](#事件订阅:从 ROI 窗口到 ViewModel)
  4. [笔刷形状:HDrawingObject 与默认圆](#笔刷形状:HDrawingObject 与默认圆)
  5. 涂抹主循环:DrawAndWipe
  6. 边界裁剪与退出重绘
  7. 画笔大小配置
  8. 小结

一、为何用 Region 而不是像素画板

  • 结果直接是 Halcon HObject 区域 ,可与 ReduceDomain、模板、特征等后续算子无缝衔接。
  • 笔刷用 仿射变换 跟随鼠标,形状可来自 GenCircle 或用户交互绘制的 HDrawingObject
  • 画 / 擦 分别对应 Union2 / Difference,语义清晰。

二、数据结构与鼠标状态

制作模板对话框 ViewModel 中维护底图、笔刷模板、最终区域,以及 Halcon 图像坐标下的鼠标位置与按键标志位。

源码: VisualPositioning/YoloAndHalcon/MainPro/ViewModels/MakeTemplateDialogViewModel.cs

csharp 复制代码
private HObject image_bk = new HObject();//图片
private HObject brush_region = new HObject();//笔刷
private HObject final_region = new HObject();//需要获得的区域
string brushType = ""; //笔刷类型

//鼠标当前位置
public double mouseCurRow = 0;
public double mouseCurCol = 0;

bool RightButtonDownFlag = false;
bool RightButtonUpFlag = false; // 右键抬起用于结束涂抹循环
bool LeftButtonDownFlag = false;

三、事件订阅:从 ROI 窗口到 ViewModel

在界面加载后,将 ROI 控件的 Halcon 鼠标移动与 WPF 鼠标键事件挂到本 ViewModel,用于更新 mouseCurRow/ColLeftButtonDownFlag,并以 右键抬起 结束涂抹模式。

csharp 复制代码
roiVm.HMouseMove -= Roivm_HMouseMove;
roiVm.MouseLeftButtonDown -= Roivm_MouseLeftButtonDown;
roiVm.MouseRightButtonDown -= RoiVm_MouseRightButtonDown;
roiVm.MouseLeftButtonUp -= Roivm_MouseLeftButtonUp;
roiVm.MouseRightButtonUp -= Roivm_MouseRightButtonUp;

roiVm.HMouseMove += Roivm_HMouseMove;
roiVm.MouseLeftButtonDown += Roivm_MouseLeftButtonDown;
roiVm.MouseRightButtonDown += RoiVm_MouseRightButtonDown;
roiVm.MouseLeftButtonUp += Roivm_MouseLeftButtonUp;
roiVm.MouseRightButtonUp += Roivm_MouseRightButtonUp;
roiVm.CanMove = false;

鼠标事件处理(更新坐标与标志位):

csharp 复制代码
private void Roivm_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    LeftButtonDownFlag = true;
}

private void RoiVm_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    LeftButtonDownFlag = false;
}

private void RoiVm_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    RightButtonUpFlag = true;
}

private void Roivm_HMouseMove(object sender, HSmartWindowControlWPF.HMouseEventArgsWPF e)
{
    mouseCurCol = e.Column;
    mouseCurRow = e.Row;
}

四、笔刷形状:HDrawingObject 与默认圆

4.1 用户在窗口中定义笔刷(矩形 / 圆等)

SetBrush 根据类型创建 HDrawingObject,初始位置在图像左上角附近,颜色设为黄色便于辨认;挂到 Halcon 窗口后用户拖拽控制点调整大小,点「确定」后 SetBrushOKGetDrawingObjectIconic() 得到 brush_region

csharp 复制代码
async void SetBrush(string brushType)
{
    foreach (var item in drawingObjList)
    {
        hSmart?.HalconWindow.DetachDrawingObjectFromWindow(item);
    }
    drawingObjList.Clear();

    this.brushType = brushType;
    HObject ho_temp_brush = new HObject();
    try
    {
        HDrawingObject.HDrawingObjectType hDrawingObjectType = HDrawingObject.HDrawingObjectType.CIRCLE;

        HTuple img_w = 0, img_h = 0;
        HOperatorSet.GetImageSize(image_bk, out img_w, out img_h);
        var size = GlobalData.Instance.saveInfo.MTCInfo.BrushSize;
        // 显示在左上角区域
        int start_row = size + 10, start_column = size + 10;
        switch (brushType)
        {
            case "矩形1":
                h_tuple = new HTuple[] { start_row, start_column, start_row + size, start_column + size };
                hDrawingObjectType = HDrawingObject.HDrawingObjectType.RECTANGLE1;
                break;
            case "矩形2":
                h_tuple = new HTuple[] { start_row, start_column, 0, size, size };
                hDrawingObjectType = HDrawingObject.HDrawingObjectType.RECTANGLE2;
                break;
            case "圆":
                h_tuple = new HTuple[] { start_row, start_column, size };
                hDrawingObjectType = HDrawingObject.HDrawingObjectType.CIRCLE;
                break;
            default:
                MessageBox.Show("错误指令");
                return;
        }
        roiVm.curDrawingObj = HDrawingObject.CreateDrawingObject(hDrawingObjectType, h_tuple);
        roiVm.curDrawingObj.SetDrawingObjectParams("color", "yellow");

        hSmart?.HalconWindow.AttachDrawingObjectToWindow(roiVm?.curDrawingObj);
        drawingObjList.Add(roiVm?.curDrawingObj);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

void SetBrushOK()
{
    try
    {
        brush_region = roiVm?.curDrawingObj.GetDrawingObjectIconic();
        hSmart?.HalconWindow.DispObj(brush_region);
        foreach (var item in drawingObjList)
        {
            hSmart?.HalconWindow.DetachDrawingObjectFromWindow(item);
        }
        drawingObjList.Clear();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

4.2 未设置笔刷时的默认圆

若尚未执行「确定」,涂抹逻辑里可用配置中的 BrushSize 生成默认圆形笔刷,并用 AreaCenter 得到仿射参考中心:

csharp 复制代码
if (!brush_region.IsInitialized())
{
    var size = GlobalData.Instance.saveInfo.MTCInfo.BrushSize;
    HOperatorSet.GenCircle(out brush_region, size, size, size);
    HOperatorSet.AreaCenter(brush_region, out areaBrush, out rowBrush, out columnBrush);
}
else
{
    HOperatorSet.AreaCenter(brush_region, out areaBrush, out rowBrush, out columnBrush);
}

五、涂抹主循环:DrawAndWipe

核心思路:

  1. 后台 Taskwhile (!RightButtonUpFlag) 轮询,约 16ms 一帧。
  2. 通过 Dispatcher.InvokeAsyncUI 线程 调用 Halcon 显示与 Region 运算。
  3. GetDomain 取图像有效域;TestRegionPoint 判断鼠标是否在域内。
  4. VectorAngleToRigid + AffineTransRegion 将笔刷平移到当前鼠标位置。
  5. 左键按下时:drawUnion2wipeDifference
  6. SetSystem("flush_graphic", "false/true") 减少闪烁。

源码摘录(与仓库一致,含边界裁剪与结束后重绘):

csharp 复制代码
async void DrawAndWipe(string actionType)
{
    // ... recover 分支略 ...

    DrawModel = true;

    HTuple areaBrush, rowBrush, columnBrush, homMat2D;
    HObject brush_region_affine = new HObject();
    try
    {
        if (!brush_region.IsInitialized())
        {
            var size = GlobalData.Instance.saveInfo.MTCInfo.BrushSize;
            HOperatorSet.GenCircle(out brush_region, size, size, size);
            HOperatorSet.AreaCenter(brush_region, out areaBrush, out rowBrush, out columnBrush);
        }
        else
        {
            HOperatorSet.AreaCenter(brush_region, out areaBrush, out rowBrush, out columnBrush);
        }

        switch (actionType)
        {
            case "draw":
                HOperatorSet.SetColor(hSmart.HalconWindow, "red");
                break;
            case "wipe":
                HOperatorSet.SetColor(hSmart.HalconWindow, "blue");
                if (!final_region.IsInitialized())
                {
                    MessageBox.Show("请先使用画出合适区域,在使用擦除功能");
                    return;
                }
                break;
            default:
                MessageBox.Show("设置错误");
                return;
        }

        var t = Task.Run(async () =>
        {
            HObject domain;
            HOperatorSet.GetDomain(image_bk, out domain);

            double lastMouseRow = double.MinValue;
            double lastMouseCol = double.MinValue;
            bool lastLeftButtonState = false;
            const int refreshDelay = 16;

            while (RightButtonUpFlag == false)
            {
                await Task.Delay(refreshDelay);

                bool mouseMoved = Math.Abs(mouseCurRow - lastMouseRow) > 0.5
                    || Math.Abs(mouseCurCol - lastMouseCol) > 0.5;
                bool leftButtonChanged = LeftButtonDownFlag != lastLeftButtonState;
                bool isFirstFrame = lastMouseRow == double.MinValue;
                bool needRedraw = mouseMoved || leftButtonChanged || isFirstFrame;

                if (!needRedraw)
                    continue;

                await Application.Current.Dispatcher.InvokeAsync(() =>
                {
                    try
                    {
                        HOperatorSet.SetSystem("flush_graphic", "false");

                        HOperatorSet.DispObj(image_bk, hSmart.HalconWindow);
                        if (final_region.IsInitialized())
                            HOperatorSet.DispObj(final_region, hSmart.HalconWindow);

                        HTuple isInside;
                        HOperatorSet.TestRegionPoint(domain, mouseCurRow, mouseCurCol, out isInside);

                        if (isInside)
                        {
                            HOperatorSet.VectorAngleToRigid(
                                rowBrush, columnBrush, 0, mouseCurRow, mouseCurCol, 0, out homMat2D);
                            brush_region_affine.Dispose();
                            HOperatorSet.AffineTransRegion(
                                brush_region, out brush_region_affine, homMat2D, "nearest_neighbor");

                            HObject clippedBrush;
                            HOperatorSet.Intersection(brush_region_affine, domain, out clippedBrush);
                            brush_region_affine.Dispose();
                            brush_region_affine = clippedBrush;

                            HOperatorSet.DispObj(brush_region_affine, hSmart.HalconWindow);

                            if (LeftButtonDownFlag == true)
                            {
                                switch (actionType)
                                {
                                    case "draw":
                                        if (final_region.IsInitialized())
                                        {
                                            HObject ExpTmpOutVar_0;
                                            HOperatorSet.Union2(
                                                final_region, brush_region_affine, out ExpTmpOutVar_0);
                                            final_region.Dispose();
                                            final_region = ExpTmpOutVar_0;
                                        }
                                        else
                                        {
                                            final_region = new HObject(brush_region_affine);
                                        }
                                        break;
                                    case "wipe":
                                        {
                                            HObject ExpTmpOutVar_0;
                                            HOperatorSet.Difference(
                                                final_region, brush_region_affine, out ExpTmpOutVar_0);
                                            final_region.Dispose();
                                            final_region = ExpTmpOutVar_0;
                                        }
                                        break;
                                }
                            }
                        }

                        HOperatorSet.SetSystem("flush_graphic", "true");
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Debug.WriteLine($"DrawAndWipe循环异常: {ex.Message}");
                    }
                }, System.Windows.Threading.DispatcherPriority.Render);

                lastMouseRow = mouseCurRow;
                lastMouseCol = mouseCurCol;
                lastLeftButtonState = LeftButtonDownFlag;
            }

            await Application.Current.Dispatcher.InvokeAsync(() =>
            {
                try
                {
                    HOperatorSet.SetSystem("flush_graphic", "false");
                    HOperatorSet.DispObj(image_bk, hSmart.HalconWindow);
                    if (final_region.IsInitialized())
                        HOperatorSet.DispObj(final_region, hSmart.HalconWindow);
                    HOperatorSet.SetSystem("flush_graphic", "true");
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Debug.WriteLine($"DrawAndWipe结束重绘异常: {ex.Message}");
                }
            }, System.Windows.Threading.DispatcherPriority.Render);

            domain?.Dispose();
        });

        await t;
    }
    finally
    {
        DrawModel = false;
        RightButtonUpFlag = false;
    }
}

说明: 完整文件中还包含 catch (HalconException)recover 清空区域等分支,上表为便于阅读做了合并;请以仓库中 MakeTemplateDialogViewModel.cs 为准。


六、边界裁剪与退出重绘

问题 做法
光标在图内但笔刷一半在图外 Intersection(brush_region_affine, domain, ...),再参与 Union2 / Difference 与显示
结束涂抹后仍看到笔刷轮廓 while 结束后仅 DispObj(image_bk) + DispObj(final_region)flush_graphic

对应源码即上节中的 Intersection循环结束后的 InvokeAsync 重绘


七、画笔大小配置

画笔像素尺寸绑定在全局配置 MTCInfo.BrushSize,由 MakeTemplateConfigInfo 暴露。

源码: VisualPositioning/YoloAndHalcon/VisionCore/Models/MakeTemplateConfigInfo.cs

csharp 复制代码
private int brushSize = 10;
/// <summary>
/// 画刷大小
/// </summary>
public int BrushSize
{
    get { return brushSize; }
    set { SetProperty(ref brushSize, value); }
}

界面侧在 MakeTemplateDialogView.xaml 的「画笔设置」分组中将 TextBox 绑定到 GlobalData.Instance.saveInfo.MTCInfo.BrushSize(具体以 XAML 为准)。


八、小结

  • 笔刷 = 模板 Region;跟随鼠标 = VectorAngleToRigid + AffineTransRegion
  • = Union2 = Difference不越界 = 与 GetDomain 得到的 domainIntersection
  • 线程 :Halcon 窗口操作放在 Dispatcher(UI 线程) ;轮询放在后台 Task
  • 体验flush_graphic 批量绘制;退出时再刷一帧去掉笔刷预览。
相关推荐
白露与泡影1 天前
Spring Cloud进阶--分布式权限校验OAuth2
分布式·spring cloud·wpf
致宏Rex1 天前
飞书 CLI 教程:官方 SDK 与文档探索,给 Agent 交互如虎添翼
交互·飞书
zandy10111 天前
从拖拽到对话:衡石Agentic BI如何重构企业数据分析的交互范式
重构·数据分析·交互
枫叶丹41 天前
【HarmonyOS 6.0】ArkData 分布式数据对象新特性:资产传输进度监听与接续传输能力深度解析
开发语言·分布式·华为·wpf·harmonyos
wuguan_1 天前
Halcon模板匹配+对齐模板+测量边(例子)
halcon·模板匹配·测量算子
一念春风1 天前
智能文字识别工具(AI)
开发语言·c#·wpf
故事不长丨2 天前
WPF MvvmLight 超详细使用教程
c#·wpf·mvvm·mvvmlight
轻口味3 天前
HarmonyOS 6 原生图表库 qCharts 深度解析:高性能、全场景交互的 ArkUI 实战
华为·交互·harmonyos
IT小哥哥呀3 天前
基于windows的个人/团队的时间管理工具
windows·c#·wpf·时间管理