用 Halcon 实现涂抹:Region、仿射变换与 WPF 交互
本文说明在 C# + Halcon + WPF 下,如何用 Region(区域) 实现类似「画笔涂抹」的模板制作交互,并附本仓库 实际源码摘录 (路径相对于仓库根目录
视觉定位)。
目录
- [为何用 Region 而不是像素画板](#为何用 Region 而不是像素画板)
- 数据结构与鼠标状态
- [事件订阅:从 ROI 窗口到 ViewModel](#事件订阅:从 ROI 窗口到 ViewModel)
- [笔刷形状:
HDrawingObject与默认圆](#笔刷形状:HDrawingObject 与默认圆) - 涂抹主循环:
DrawAndWipe - 边界裁剪与退出重绘
- 画笔大小配置
- 小结
一、为何用 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/Col 与 LeftButtonDownFlag,并以 右键抬起 结束涂抹模式。
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 窗口后用户拖拽控制点调整大小,点「确定」后 SetBrushOK 用 GetDrawingObjectIconic() 得到 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
核心思路:
- 后台
Task中while (!RightButtonUpFlag)轮询,约 16ms 一帧。 - 通过
Dispatcher.InvokeAsync在 UI 线程 调用 Halcon 显示与 Region 运算。 GetDomain取图像有效域;TestRegionPoint判断鼠标是否在域内。VectorAngleToRigid+AffineTransRegion将笔刷平移到当前鼠标位置。- 左键按下时:
draw→Union2;wipe→Difference。 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得到的domain做Intersection。 - 线程 :Halcon 窗口操作放在 Dispatcher(UI 线程) ;轮询放在后台
Task。 - 体验 :
flush_graphic批量绘制;退出时再刷一帧去掉笔刷预览。