在做一些线扫相机且进行连续拍摄的项目时,由于图像扫描的随机性,部分场景下需要对图像进行拼接和裁切,获取完整的一个图像。由于halcon中 crop相关的算子都是开辟新的内存方式,为了避免内存重复开辟,此处提供一个逻辑方式如下:
优点:
1)不需要每次都去新建内存,每次只需要做mem_copy的动作,减少耗时
2)在完整图像中查找特征,避免特征由于随机性拍照不完整而丢失
缺点:
1)如果直接复用指针,可能会导致拼接输出图像频率>检测频率,即缓存[0]的图片输出后,但检测未完成,此处队列循环又来到了缓存[0],最终导致原本的缓存[0]图像被覆盖,图像信息丢失。解决方式:可以修改为新建内存输出,会浪费一些cpu和内存
内存操作相关代码:
public static class IntPtrHelper
{
///
/// 拷贝指针数据
///
///
///
///
[DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
///
/// 指针数据集重置
///
///
///
///
///
[DllImport("msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
public static extern IntPtr MemSet(IntPtr dest, int c, int count);
///
/// 从字节中拷贝数据至指针中
///
///
///
///
///
public static void CopyFromBytes(this IntPtr destination, byte[] sourcesBytes,int startIndex,int length)
{
Marshal.Copy(sourcesBytes, startIndex, destination, length);
}
///
/// 获取指针内指定位置的内容的字节
///
///
///
///
///
public static byte[] ToBytes(this IntPtr sourcePtr,int sourceOffset,int length)
{
byte[] rlt = new byte[length];
Marshal.Copy(sourcePtr.Offset(sourceOffset), rlt, 0, length);
return rlt;
}
///
/// 从一个指针的数据拷贝到另一个指针中
///
/// 数据源指针
/// 目标指针
/// 数据源指针偏移值
/// 目标指针偏移值
/// 拷贝长度
public static void Copy(IntPtr source, IntPtr destination, int sourceOffset, int destinationOffset, int length)
{
IntPtr destPtr = destination.Offset(destinationOffset),
srcPtr = source.Offset(sourceOffset);
CopyMemory(destPtr, srcPtr, (uint)length);
}
///
/// 指针偏移指定长度
///
///
///
///
public static IntPtr Offset(this IntPtr sourcePrt, int offset)
{
IntPtr rltPtr;
if (IntPtr.Size == sizeof(long))
{
rltPtr = new IntPtr(sourcePrt.ToInt64() + offset);
}
else
{
rltPtr = new IntPtr(sourcePrt.ToInt32() + offset);
}
return rltPtr;
}
///
/// 拷贝源指针指定位置的内容,返回新的指针.注意:此处的指针需要自己释放,不纳入内存管理
///
///
///
///
///
public static IntPtr Copy(IntPtr source, int offset, int length)
{
IntPtr ptr = Marshal.AllocHGlobal(length);
Copy(source, ptr, offset, 0, length);
return ptr;
}
}
设置根据图片 宽度/高度/通道数/缓存个数 创建一个循环队列,每次在完整一张图片拼接截取后,自动移到下一张图片,假设缓存个数为3,则就是 [0,1,2,0,1,2,0....]这样无限循环取图片缓存进行内存拷贝覆盖,代码如下
///
/// 拷贝图像
///
/// 原始图像
/// 需拷贝到的图像位置
///
///
///
///
///
public static void Copy(HImage source, HImage destination, int sourceOffset, int destinationOffset, int length,emImageType imageType, emVerticalImageChannel imageChannel)
{
KeyValuePair[] ptrParis = null;
if(imageChannel == emVerticalImageChannel.one)
{
//单通道
IntPtr ptrSrc = source.GetImagePointer1(out string _, out int width, out int height);
IntPtr ptrDestination = destination.GetImagePointer1(out string _, out width, out height);
ptrParis = new KeyValuePair[1] {
new KeyValuePair(ptrSrc,ptrDestination)
};
}
else
{
//三通道
source.GetImagePointer3(out IntPtr ptr1Src, out IntPtr ptr2Src, out IntPtr ptr3Src, out string _, out int width, out int height);
destination.GetImagePointer3(out IntPtr ptr1Destination, out IntPtr ptr2Destination, out IntPtr ptr3Destination, out string _, out width, out height);
ptrParis = new KeyValuePair[3] {
new KeyValuePair(ptr1Src,ptr1Destination),
new KeyValuePair(ptr2Src,ptr2Destination),
new KeyValuePair(ptr3Src,ptr3Destination),
};
}
int pixelByteNumber = imageType.GetPixelByteNumber(); //获取图片类型一个像素对应的字节数
foreach(KeyValuePair kvp in ptrParis)
{
//循环拷贝
IntPtrHelper.Copy(kvp.Key, kvp.Value, sourceOffset* pixelByteNumber, destinationOffset* pixelByteNumber, length* pixelByteNumber);
}
}
此处需要自己实现算法,最终目的就是找到你要的图片的起始行坐标和终止行坐标
有了起始行坐标和终止行坐标后,我们只需要拿出对应的内存指针,调用GenImage1Extern或GenImage3Extern进行图片生成即可。需要注意的是要用HImage2来使用图像,因为假设原始那张缓存图片被释放掉,那么就会出现访问野指针的情况,毁灭性打击。
public class HImage2:HImage
{
///
/// 引用的图像
///
public HImage ReferenceData { get; set; }
public HImage2(HImage referenceImage, HObject hObject):base(hObject)
{
//智能指针,不需要拷贝内存新建的问题,此处避免由于缓存中图像释放导致此处访问野指针的问题
ReferenceData = new HImage(referenceImage);
}
public override void Dispose()
{
base.Dispose();
ReferenceData?.Dispose();
ReferenceData = null;
}
}
///
/// 根据垂直方向的位置的长度,进行图像裁切,且图像引用原来的指针
///
///
/// 起始行坐标
/// 高度
///
public static HImage2 CropVerticalReference(this HImage image,int top,int height)
{
int channel = image.CountChannels().I;
image.GetImageSize(out int widthSrc, out int heightSrc);
if(top+height>heightSrc)
{
throw new Exception($"裁切终止行为{top + height},超出图像高度{heightSrc}");
}
if(channel == 1)
{
IntPtr ptr = image.GetImagePointer1(out string type, out widthSrc, out heightSrc);
emImageType typeEm = (emImageType)Enum.Parse(typeof(emImageType), type, true);
int scale = typeEm.GetPixelByteNumber();
IntPtr offsetPtr = ptr.Offset(scale * top * widthSrc);
HOperatorSet.GenImage1Extern(out HObject temp, type, widthSrc, height, offsetPtr, 0);
HImage2 rlt = new HImage2(image, temp);
temp.Dispose();
return rlt;
}
else if(channel == 3)
{
image.GetImagePointer3(out IntPtr pRed, out IntPtr pGreen, out IntPtr pBlue, out string type, out widthSrc, out heightSrc);
emImageType typeEm = (emImageType)Enum.Parse(typeof(emImageType), type, true);
int scale = typeEm.GetPixelByteNumber();
IntPtr offsetPtrRed = pRed.Offset(scale * top * widthSrc);
IntPtr offsetPtrGreen = pGreen.Offset(scale * top * widthSrc);
IntPtr offsetPtrBlue = pBlue.Offset(scale * top * widthSrc);
HOperatorSet.GenImage3Extern(out HObject temp, type, widthSrc, height, offsetPtrRed, offsetPtrGreen, offsetPtrBlue, 0);
HImage2 rlt = new HImage2(image, temp);
temp.Dispose();
return rlt;
}
else
{
throw new Exception($"不支持通道数为{channel}的图像裁剪.");
}
}
该方式适用于大图像的垂直方向拼接并截取检测,减少内存消化,同时避免野指针的问题。