背景
在做一些线扫相机且进行连续拍摄的项目时,由于图像扫描的随机性,部分场景下需要对图像进行拼接和裁切,获取完整的一个图像。由于halcon中 crop相关的算子都是开辟新的内存方式,为了避免内存重复开辟,此处提供一个逻辑方式如下:
优点:
1)不需要每次都去新建内存,每次只需要做mem_copy的动作,减少耗时
2)在完整图像中查找特征,避免特征由于随机性拍照不完整而丢失
缺点:
1)如果直接复用指针,可能会导致拼接输出图像频率>检测频率,即缓存[0]的图片输出后,但检测未完成,此处队列循环又来到了缓存[0],最终导致原本的缓存[0]图像被覆盖,图像信息丢失。解决方式:可以修改为新建内存输出,会浪费一些cpu和内存
内存操作相关代码:
public static class IntPtrHelper
{
/// <summary>
/// 拷贝指针数据
/// </summary>
/// <param name="dest"></param>
/// <param name="src"></param>
/// <param name="count"></param>
[DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
/// <summary>
/// 指针数据集重置
/// </summary>
/// <param name="dest"></param>
/// <param name="c"></param>
/// <param name="count"></param>
/// <returns></returns>
[DllImport("msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
public static extern IntPtr MemSet(IntPtr dest, int c, int count);
/// <summary>
/// 从字节中拷贝数据至指针中
/// </summary>
/// <param name="destination"></param>
/// <param name="sourcesBytes"></param>
/// <param name="startIndex"></param>
/// <param name="length"></param>
public static void CopyFromBytes(this IntPtr destination, byte[] sourcesBytes,int startIndex,int length)
{
Marshal.Copy(sourcesBytes, startIndex, destination, length);
}
/// <summary>
/// 获取指针内指定位置的内容的字节
/// </summary>
/// <param name="sourcePtr"></param>
/// <param name="sourceOffset"></param>
/// <param name="length"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 从一个指针的数据拷贝到另一个指针中
/// </summary>
/// <param name="source">数据源指针</param>
/// <param name="destination">目标指针</param>
/// <param name="sourceOffset">数据源指针偏移值</param>
/// <param name="destinationOffset">目标指针偏移值</param>
/// <param name="length">拷贝长度</param>
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);
}
/// <summary>
/// 指针偏移指定长度
/// </summary>
/// <param name="sourcePrt"></param>
/// <param name="offset"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 拷贝源指针指定位置的内容,返回新的指针.注意:此处的指针需要自己释放,不纳入内存管理
/// </summary>
/// <param name="source"></param>
/// <param name="offset"></param>
/// <param name="length"></param>
/// <returns></returns>
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....]这样无限循环取图片缓存进行内存拷贝覆盖,代码如下
/// <summary>
/// 拷贝图像
/// </summary>
/// <param name="source">原始图像</param>
/// <param name="destination">需拷贝到的图像位置</param>
/// <param name="sourceOffset"></param>
/// <param name="destinationOffset"></param>
/// <param name="length"></param>
/// <param name="imageType"></param>
/// <param name="imageChannel"></param>
public static void Copy(HImage source, HImage destination, int sourceOffset, int destinationOffset, int length,emImageType imageType, emVerticalImageChannel imageChannel)
{
KeyValuePair<IntPtr, IntPtr>[] 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<IntPtr, IntPtr>[1] {
new KeyValuePair<IntPtr, IntPtr>(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<IntPtr, IntPtr>[3] {
new KeyValuePair<IntPtr, IntPtr>(ptr1Src,ptr1Destination),
new KeyValuePair<IntPtr, IntPtr>(ptr2Src,ptr2Destination),
new KeyValuePair<IntPtr, IntPtr>(ptr3Src,ptr3Destination),
};
}
int pixelByteNumber = imageType.GetPixelByteNumber(); //获取图片类型一个像素对应的字节数
foreach(KeyValuePair<IntPtr,IntPtr> kvp in ptrParis)
{
//循环拷贝
IntPtrHelper.Copy(kvp.Key, kvp.Value, sourceOffset* pixelByteNumber, destinationOffset* pixelByteNumber, length* pixelByteNumber);
}
}
二、查找特征确定行起始和行终点
此处需要自己实现算法,最终目的就是找到你要的图片的起始行坐标和终止行坐标
三、输出图像
有了起始行坐标和终止行坐标后,我们只需要拿出对应的内存指针,调用GenImage1Extern或GenImage3Extern进行图片生成即可。需要注意的是要用HImage2来使用图像,因为假设原始那张缓存图片被释放掉,那么就会出现访问野指针的情况,毁灭性打击。
public class HImage2:HImage
{
/// <summary>
/// 引用的图像
/// </summary>
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;
}
}
/// <summary>
/// 根据垂直方向的位置的长度,进行图像裁切,且图像引用原来的指针
/// </summary>
/// <param name="image"></param>
/// <param name="top">起始行坐标</param>
/// <param name="height">高度</param>
/// <returns></returns>
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}的图像裁剪.");
}
}
总结
该方式适用于大图像的垂直方向拼接并截取检测,减少内存消化,同时避免野指针的问题。