拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C#上位机必看:ROI剪切图像转换为Halcon HObject的完整方案

简介一套面向C#与Halcon联合编程的机器视觉源码示例针对CCD相机图像中感兴趣区域ROI的剪切与格式转换需求帮助开发者快速实现从图像采集到Halcon图像对象HImage的衔接。Demo工程以WinForms界面为载体覆盖界面代码、程序入口与资源配置文件便于直接对照学习或二次开发。压缩包共39个文件以cs源码、dll库、exe及pdb调试信息为主整体大小约18.17MB目录结构清晰。已有1478人学习下载适合具备一定C#基础、希望在Halcon环境下实践ROI裁剪与图像预处理的机器视觉开发人员。通过Demo可理解HImage与HRegion对象的配合方式以及从图像加载、ROI定义到裁剪输出的完整写法便于移植到CCD视觉检测、目标定位等实际项目中。 做C#上位机开发的人迟早会和Halcon打交道。尤其是视觉定位、模板匹配、产品缺陷检测这类项目界面逻辑用C#写图像算法交给Halcon这是工业视觉里最常见的一套组合。但真正联调的时候有一个绕不开的环节相机采集的画面在C#侧往往是一个Bitmap而算法侧需要的是Halcon的HObject。如果只是把整帧图丢过去还好说一旦牵扯到ROI剪切很多刚开始做联合开发的朋友就卡住了。C#的Rectangle和Halcon的Region、HObject之间远没有想象中那么“无缝”。这篇内容就是围绕“C#将ROI剪切图像转换为Halcon图像”这条主线把我在实际项目里验证过的转换思路、踩过的坑、以及可以复用的代码一次性讲清楚。1. 先说清楚ROI剪切后为什么要单独转一次1.1 直传整图的隐藏成本很多人在HDevelop里调试习惯了直接读一张整图然后用reduce_domain配合ROI区域把感兴趣的地方截出来再交给后面的定位、测量算子。这套流程在HDevelop的脚本环境里没有任何问题但放到C#上位机里就变味了。原因很简单HDevelop里你有一个已经存在的HObject截取ROI是对HObject做域操作。但C#侧拿到的是相机SDK回调过来的图像可能是Bitmap可能是byte[]甚至是一个IntPtr指针。你如果先花时间把这整帧图转成HObject再去做ReduceDomain和CropDomain等于先把一整张500万像素的图拷贝进Halcon内部再二次拷贝出ROI区域。这个过程中显存和内存的带宽都花在了“你不关心的背景区域”上。在视觉项目里ROI通常只占全图的十分之一甚至更小。为了这十分之一的数据去搬运十分之十的原图产线上几十毫秒的节拍就是这么被吃掉的。所以我个人在做C#联合Halcon的项目时只要事先已知ROI坐标一律在C#侧先剪裁再把剪裁后的小图转成HObject。1.2 两条常规路线的取舍C#里将ROI剪切图像转换为Halcon图像目前常见做法有两条路我画个最简单的对比方案做法优点缺点路线AC#先用GDI把ROI剪成新Bitmap再LockBits拿像素转HObject代码直观坐标逻辑好调试兼容各种输入源多生成一张Bitmap对象内存开销稍高路线B直接从相机原始buffer或Bitmap的Scan0指针里按行截取ROI数据构造HObject少一次Bitmap拷贝速度快内存占用低需要对Stride、像素格式、内存布局非常清楚我的建议是如果是项目前期功能验证、ROI坐标经常人工调整先走路线A开发效率最高如果是设备量产、相机连续采集每帧都要做ROI提取走路线B更合适。这篇博文主要把路线A讲透同时把路线B的关键代码也写出来你在项目里可以按需切换。1.3 本方案的核心链路路线A的整体链路可以概括成一句话Bitmap原图 → Graphics.DrawImage剪出ROI → LockBits锁定像素 → 逐行剔除Stride对齐字节 → byte[] → GenImage1 → HObject。链路听起来不复杂但每一步都有值得注意的细节。尤其是Stride对齐问题我第一次写的时候没注意生成的Halcon图像直接从中间斜切了一刀排查了半天才发现是C#的Bitmap行数据按4字节对齐惹的祸。下面两个章节我按实操顺序把每一步拆开讲。2. 实操第一步在C#侧把ROI剪出来2.1 用GDI剪切还是直接LockBits如果你已经在界面上画了一个ROI框坐标是确定的最简单的做法就是用Graphics.DrawImage把原图的那块矩形区域画到一个新Bitmap上private Bitmap CropBitmap(Bitmap src, Rectangle roi) { Bitmap roiBmp new Bitmap(roi.Width, roi.Height, PixelFormat.Format24bppRgb); using (Graphics g Graphics.FromImage(roiBmp)) { g.DrawImage(src, new Rectangle(0, 0, roi.Width, roi.Height), roi, GraphicsUnit.Pixel); } return roiBmp; }这段代码干的事情很清楚把src里roi区域的像素原样画到新图的左上角。这样做的好处是原图是灰度还是彩色都不重要GDI会帮你处理坐标映射ROI超出图像边界也只是黑边不会直接崩溃。如果你的项目里不需要这层封装相机SDK回调里拿到的本来就是byte[]再转成Bitmap反而多一次拷贝。这种场景更适合直接通过指针操作取出ROI数据// 假设原图像素格式为 bgra通道数为 4 public static byte[] CropRawBuffer(byte[] srcBuffer, int srcWidth, int srcHeight, int channels, Rectangle roi) { int roiWidth roi.Width; int roiHeight roi.Height; byte[] result new byte[roiWidth * channels * roiHeight]; for (int row 0; row roiHeight; row) { int srcRowStart (roi.Y row) * srcWidth * channels roi.X * channels; int dstRowStart row * roiWidth * channels; Buffer.BlockCopy(srcBuffer, srcRowStart, result, dstRowStart, roiWidth * channels); } return result; }这种方式拿到的是连续、紧凑的像素数组后面直接交给Halcon的GenImage1或者GenImageInterleaved都能用。没有Bitmap参与也就没有Stride对齐的困扰。2.2 避坑点Bitmap格式与Stride路线A里只要用了LockBits就一定会碰到BitmapData.Stride这是新手最容易翻车的地方。Stride可以理解为Bitmap每一行像素在内存里实际占用的字节数。GDI为了提高内存访问效率会按4字节对齐每一行。比如一张宽度为7像素、24位真彩色的图一行像素理论占用7 * 3 21字节但实际上Stride往往是24字节多出来的3个字节就是填充位不存储任何像素。新手常见的错误是一次性拷贝int stride bmpData.Stride; byte[] pixels new byte[stride * height]; Marshal.Copy(bmpData.Scan0, pixels, 0, pixels.Length); image.GenImage1(byte, width, height, pixels);这段代码在width * 3 stride的时候能正常跑但图像宽度不是4的倍数时Halcon拿到的像素就是斜的。因为Halcon的GenImage1认为每行就是width * channels个字节不会理会C#侧的对齐填充。正确做法是逐行拷贝把每行的有效像素剔出来重新排列成紧凑数组private HObject ConvertCroppedBitmapToHObject(Bitmap roiBmp) { int width roiBmp.Width; int height roiBmp.Height; const int channels 3; BitmapData bmpData roiBmp.LockBits( new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { int stride bmpData.Stride; byte[] buffer new byte[width * channels * height]; for (int row 0; row height; row) { IntPtr srcRowPtr IntPtr.Add(bmpData.Scan0, row * stride); Marshal.Copy(srcRowPtr, buffer, row * width * channels, width * channels); } HObject image new HObject(); image.GenImage1(byte, width, height, buffer); return image; } finally { roiBmp.UnlockBits(bmpData); } }这一步做好之后像素数组已经是紧凑排列了后面怎么转都不会出现错位。个人建议把这段逐行拷贝的逻辑封装成一个独立函数项目里多处复用。3. 实操第二步将byte[]转成Halcon HObject3.1 GenImage1的最小可用代码拿到紧凑排列的byte[]后转HObject的核心就是一个算子HObject image new HObject(); image.GenImage1(byte, width, height, buffer);这里GenImage1的第一个参数是像素类型灰度图用byte就行对应C#里的byte取值范围0~255。第二个和第三个参数是图像宽高第四个参数是像素数组。有一个容易被忽略的点GenImage1传入byte[]后HalconDotNet包装层会拷贝一份数据到Halcon管理的内存里。也就是说函数返回后C#侧的这个buffer数组可以安全释放或者复用不用一直保留着。这一点和直接传指针的方案完全不同——传IntPtr时Halcon不会拷贝数据如果你把托管数组释放了后续再访问这个HObject轻则花屏重则直接崩溃。所以如果你是走byte[]这条路线函数的写法可以很安全public static HObject ByteToHObjectGray(byte[] grayPixels, int width, int height) { HObject image new HObject(); image.GenImage1(byte, width, height, grayPixels); return image; }调用方只需要记得用完之后释放返回的HObject即可。3.2 彩色图怎么办BGR顺序与GenImageInterleaved如果你的ROI是彩色图问题就多了一层通道顺序。C#的PixelFormat.Format24bppRgb虽然名字叫RGB但它存储在内存里实际顺序是BGR。直接把它当rgb传给Halcon你会发现图像的红色和蓝色通道对调了。处理方式有两种。第一种是先把彩色Bitmap转成灰度再走GenImage1适合后端算法本身只需要灰度图的场景Bitmap grayBmp new Bitmap(roiBmp.Width, roiBmp.Height, PixelFormat.Format8bppIndexed); // 用 ColorMatrix 或 LockBits 手动灰度化这里省略细节第二种是保留彩色信息使用GenImageInterleaved并明确告知Halcon底层的通道顺序是BGRHObject colorImage new HObject(); colorImage.GenImageInterleaved(pixels, bgr, width, height, 0, byte, width, height, 8, 0, 0, -1);我实际项目里用得更多的还是灰度图。工业视觉里的模板匹配、缺陷检测绝大多数算法跑在灰度图上就够了彩色图转换成本高后续算子处理也慢。当然如果产品外观检测必须依赖颜色信息那就老老实实走GenImageInterleaved只需要把通道顺序写对就行。3.3 内存问题谁负责释放什么时候释放C#里有GC很多新接触Halcon开发的程序员会天然地把HObject也当成托管对象觉得用完了放着不管就行。这是大坑。Halcon的HObject在HalconDotNet里虽然实现了IDisposable但它底层的图像数据是Halcon运行时分配的不归.NET的GC直接托管。如果你的程序不停创建HObject却从不释放内存会一路涨上去最终在某个DispObj或者模板匹配算子上报一个莫名的Halcon错误。我常用的写法是两种。第一种处理单张图用using块using (HObject roiImage ConvertCroppedBitmapToHObject(roiBmp)) { HOperatorSet.ReduceDomain(roiImage, roiRegion, out HObject imageReduced); // 后续处理 }第二种在循环采集场景下保留一个成员变量每次处理前先释放上一次的private HObject _roiImage; private void OnFrameCaptured(Bitmap frame) { Rectangle roi GetCurrentRoi(); using (Bitmap roiBmp CropBitmap(frame, roi)) { _roiImage?.Dispose(); _roiImage ConvertCroppedBitmapToHObject(roiBmp); } // 交给视觉处理线程 }这里的关键是创建HObject的地方和处理HObject的地方可能不在同一个函数里一定要明确谁负责Dispose。我的习惯是“谁创建谁负责释放跨线程传递时接收方用完后也要释放”并且在代码注释里写清楚免得后来维护的人一脸懵。4. 实测数据与应用扩展4.1 整图转再Crop vs 先剪ROI再转我在一个实际的定位项目里做过一组简单对比。相机是500万像素黑白相机图像尺寸2448×2048ROI区域大约500×400像素连续采集100帧分别跑两种方案。方案单帧平均耗时内存拷贝量整图Bitmap转HObject再用ReduceDomainCropDomain约28ms整幅图约5MB拷贝两次C#先剪ROI再把500×400的byte[]转成HObject约3ms仅ROI约0.2MB拷贝一次两种方案在100帧累计耗时上差了将近10倍。而且后续如果要做模板匹配小图构造的HObject在CreateShapeModel和FindShapeModel阶段也会更快因为待匹配的像素量本身少了一个数量级。当然这不是说ReduceDomain没用。在HDevelop脚本里配合鼠标交互画ROIReduceDomain仍然是最高效的开发方式。但在C#上位机里坐标都已经在界面层确定好了完全没必要把这些坐标再转成Halcon的Region然后又切一次图。4.2 扩展场景模板匹配、掩膜、深度学习标注这套“C#剪ROI再转HObject”的思路除了基础显示之外还能直接嫁接到几个很常见的场景。第一个是模板匹配。有些项目需要在ROI内建立模板直接把剪裁后的图像传给CreateShapeModel模板区域从一开始就限定好了比在全图上建模板再抠细节要干净得多。第二个是掩膜和排除干扰点。Halcon里做匹配或测量经常需要指定一个掩膜区域把不需要参与计算的纹理、脏点排除掉。在C#上位机里你可以用Bitmap或者直接绘制一个Region的轮廓再用类似方式把掩膜转成HObject的Region和ROI图像一起交给Halcon。这样整套流程从UI到算法都是统一的坐标体系调试起来直观很多。第三个是深度学习的图像预处理。如果你在C#里加载一个标注文件里面是矩形框坐标需要裁出目标区域送进模型推理完全可以直接用这里的CropToHObject方法把裁好的图交给Halcon的深度学习预处理算子。相比先整图转再CropRectangle1代码量更少内存占用也更可控。5. 我踩过的三个坑常见问题排查5.1 图像倒置或错位Halcon图像的左上角是坐标原点行方向从上到下。C#的Bitmap在内存里通常也是从上到下存储。两者一致按理不该出错。但如果是自己写byte[]组装HObject最常见的问题就是把各行的起始位置算错了尤其是从相机SDK的buffer里直接截ROI时坐标没对齐出来的图就是斜的或者整体平移的。排查思路很固定先用一个图案简单、边界清晰的图比如一张白底黑块图做测试把原图和转换后的Halcon图都保存下来肉眼比对。如果只有颜色对但内容斜了优先检查Stride对齐如果内容位置偏移了检查ROI坐标是相对整图还是相对某个子区域计算的。5.2 Halcon窗口显示一片黑代码跑起来不报错图像窗口里却一片黑这种问题多半出在“显示”和“数据”脱节上。比如你在C#里拿到HObject后直接DispObj但窗口的HWindowControl还没完成初始化或者HObject本身是空的只是GenEmptyObj声明了变量没有实际赋值。还有一个容易忽略的原因是彩色图直接用DispObj显示Halcon可能会以RGB顺序解释图像数据而你的内存顺序是BGR红蓝通道对调后在某些灰度主题下看起来就像一团黑糊糊的色块。遇到这种问题先调用WriteImage把HObject保存成PNG或BMP文件看文件内容是否正确能立刻区分是数据问题还是显示问题。5.3 内存只增不减连续跑了几千帧之后内存涨了几个GB任务管理器一看都快爆了但是代码看起来又没new几个大对象。这种问题我在早期项目里遇到过不止一次原因基本都是同一个HObject没有释放。最常见的是循环里用了局部变量while (running) { HObject img ConvertCroppedBitmapToHObject(roiBmp); // 忘了 img.Dispose(); 直接进下一轮循环 }每一轮循环生成的HObject都泄漏一点几千帧下来内存必然涨上去。排查方法也不难一段代码里临时加个计数器在Dispose前后打印HObject的引用计数或者直接看任务管理器内存在循环跑的时候是不是只升不降。如果你不确定某个HObject该不该释放最简单粗暴的原则就是这个变量一旦不再使用就立刻Dispose()宁可多释放一次也别少释放一次。我在实际写代码时习惯把转换逻辑封装成一个专门的静态工具类统一处理Bitmap和byte[]到HObject的转换同时约定所有返回HObject的方法调用方必须负责释放。这样虽然不能完全杜绝泄漏但至少排查范围被大大缩小了。最后再分享一个个人习惯如果你的项目里有多个相机或者图像源建议在图像采集回调里尽早把数据转成统一的HObject而不是到处传Bitmap再到处转。这样整个图像处理链路的数据形式是一致的后续写模板匹配、测量、深度学习预处理都会顺畅很多。ROI剪裁虽然看起来是个小功能但它是C#和Halcon之间的第一道桥桥搭稳了后面跑的算法才能真正常态化稳定运行。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门