因为工作需要,我一个学电气的终于要涉足图像识别领域了吗?
并不,只是做了一个简单的Demo,实现预期目标之后写篇笔记记录一下过程以备后用。基本功能是利用海康威视设备网络SDK连接局域网内的摄像机,实时预览画面的同时监控画面的异常状况,监测到异常状况后向PLC输出开关量报警信号,同时抓拍异常画面。这个SDK除预览和抓拍以外还提供了录像,云台控制,布防等接口。本Demo实现了同时监控4路视频,其实更多路也行,原理相同,4路和40路只是差一点工作量而已。老规矩,先上图。
初期准备
开始之前首先要一个备的准。
- 海康威视网络摄像机一台(基于海康SDK开发)
- 电脑一台
(废话) - 叫花鸡(jiāo huàn jī,手抖打错,感觉挺有意思就不改了,非必须)
- 支持S7协议的PLC一台(做这个东西的本意就是报警输出至PLC,否则用自带软件就可以了)
- C#开发环境
- .NET Framework 4.5.2以上(本文4.7.2)
- 海康SDK(官网提供下载)
硬件连接只是一根网线和一根电源,如果有PoE交换机的话连电源都省了。确保电脑和摄像机在同一网络下即可,摄像机出厂默认IP:192.168.1.64
。新摄像机在使用前需要初始化(激活),可以直接在浏览器输入摄像机IP进入管理页面,也可以在海康官网下载SADP软件。初始化需要设置账户名称和密码,如有改IP与端口号的需求也可以直接在SADP内修改。
海康的SDK包里除了库与文档之外,还很贴心的放了很多Demo,可以直接拿来二次开发,很是方便。既然有就没必要从头来一遍了,不过其实也不难,按着文档用几个方法就能实现。使用前需要将库文件全部复制到调试路径下,Demo就可以直接使用了。如果出错,可以在文档里查错误码,前面步骤没错的话,很有可能是ip设置的问题。
处理图片
处理图片的逻辑十分简单,谈不上图像识别或人工智能等等高深的词汇。总体流程就是抓一张图放进内存,算出它的特征码,再抓一张,再算一次。然后对比两个特征码的区别。
预备…开工!
抓取图片
初始化和连接摄像头直接用Demo里现成的就好。为了实现实时对比,可能需要几百或几十毫秒就抓一张图,写一个抓图的函数方便调用。
抓图前需要创建一个存储图片信息的区域,用NET_DVR_JPEGPARA
设定抓图的格式与质量,此处列举几个常用数值,更多信息参见开发手册。
wPicQuality 图片质量系数:0-最好,1-较好,2-一般
wPicSize 图片尺寸:5-HD720P(1280*720),9-HD1080P(1920*1080),0xff-Auto(使用当前码流分辨率)
private Image JpgtoBuffer()
{
byte[] jpgBytes = new byte[500000];//1920*1080的jpg图像大小约400000字节
UInt32 sizeReturned = 0;
int lChannel = 1; //通道号
CHCNetSDK.NET_DVR_JPEGPARA lpJpegPara = new CHCNetSDK.NET_DVR_JPEGPARA
{
wPicQuality = 0, //图像质量
wPicSize = 0xff //抓图分辨率
};
if (!CHCNetSDK.NET_DVR_CaptureJPEGPicture_NEW(handle,lChannel,ref lpJpegPara,jpgBytes, (uint)jpgBytes.Length,ref sizeReturned))
{
iLastErr = CHCNetSDK.NET_DVR_GetLastError();
str = "失败,错误代码:" + iLastErr;
MessageBox.Show(str, "提示");
return null;
}
byte[] realBytes = new byte[sizeReturned];
Buffer.BlockCopy(jpgBytes, 0, realBytes, 0, realBytes.Length);
MemoryStream ms = new MemoryStream(realBytes);
Image pic = Image.FromStream(ms);
return pic;
}
C#抓图使用了NET_DVR_CaptureJPEGPicture_NEW
,功能为:单帧数据捕获并保存成JPEG存放在指定的内存空间中。需要登录的返回值(即连接摄像机的句柄),通道号。该函数直接返回图片,也可以声明一个全局Image
变量,将此函数返回类型改为void
。
缩小尺寸
采集的图片尺寸为1920*1080px,像素过多,处理起来会很慢,而且实现简单的判断也不需要如此多的数据,可以先缩小图片,既能减小计算量,提高效率,又能节约资源。
缩小图片的代码一行就够:
private Image smallPic(Image bigPic)
{
Image newsmall = bigPic.GetThumbnailImage(32, 32, () => { return false; }, IntPtr.Zero);
return newsmall;
}
C#根据需求,可以修改缩放后的尺寸,检测对象比较明显的话,甚至可以将尺寸调整为8*8,减少计算量的同时可以过滤掉影响判断的细节,减小误判的可能。
计算灰度
将缩小后的图片转为灰度图像,便于计算特征。RGB图像计算灰度的方法是:灰度值=R*0.3+G*0.59+B*0.11
,依据是人眼对不同颜色的敏感度。遍历图像的每一个像素,获取RGB值,计算出灰度值之后重新赋值予RGB,图像就成为了灰度图像。
private Byte[] ToGray(Image input)
{
Bitmap bit = new Bitmap(input);
Byte[] gray = new Byte[input.Width * input.Height];
for (int i = 0; i < input.Width; i++)
{
for (int j = 0; j < input.Height; j++)
{
Color color = bit.GetPixel(i, j);
byte grayvalue = (Byte)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11);
gray[i * input.Width + j] = grayvalue;
}
}
return gray;
}
C#这种传统计算灰度的方法在计算每一个像素前都要读取一次(.GetPixel();
),这个方法速度很慢,像素比较多的时候会显著推慢运行速度,所以在此列出更加快的方法:颜色矩阵。
private Bitmap GrayScale(Bitmap original)
{
Bitmap newBitmap = new Bitmap(original.Width, original.Height);
Graphics g = Graphics.FromImage(newBitmap);
ColorMatrix colorMatrix = new ColorMatrix(
new float[][]
{
new float[]{.30f,.30f,.30f,0,0 },
new float[]{.59f,.59f,.59f,0,0 },
new float[]{.11f,.11f,.11f,0,0 },
new float[]{0,0,0,1,0 },
new float[]{0,0,0,0,1 }
});
ImageAttributes attributes = new ImageAttributes();
attributes.SetColorMatrix(colorMatrix);
g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height), 0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes);
g.Dispose();
return newBitmap;
}
C#矩阵让我回想起大学时代的线性代数,一想到头就开始疼了万恶的高等数学。不过这个方法的执行速度简直起飞,不得不说一句真香。
计算指纹
接下来就是获取图片的特征,暂且称之为图片的指纹。逻辑是计算图片上所有像素的灰度平均值,然后每个像素一位一位与平均值对比,如果高于平均就记"1"
,否则记"0"
,可以获取一个只有0和1的字符串。然后将两张图的字符串对比,就可以获得产生变化的像素点了。如果不同的位数大于一定值,说明图片的变化率已经高于多少了。以此判断画面的变化程度。
计算平均值,很简单,加起来除一下就是:
private Byte GrayAvg(Byte[] values)
{
int sum = 0;
for(int i = 0; i < values.Length; i++)
{
sum += (int)values[i];
}
return Convert.ToByte(sum / values.Length);
}
C#然后按位对比:
private string GrayHash(byte[] value, byte avg)
{
char[] result = new char[value.Length];
for(int i = 0; i < value.Length; i++)
{
if (value[i] > avg)
{
result[i] = '1';
}
else
{
result[i] = '0';
}
}
string hash = new string(result);
return hash;
}
C#从获取到第二个指纹开始,就可以进行对比了:
private Int32 CountSameNum(string last, string now)
{
int count = 0;
if (last.Length != now.Length)
{
StatusLabel.Text = "运算错误";
}
else
{
for (int i = 0; i < last.Length; i++)
{
if (last[i] != now[i])
{
count++;
}
}
}
return count;
}
C#获取的数值就是变化明显的像素数,除以总像素数就是图片变化率。
输出信号
光判断出来变化在电脑上显示出来还不够,在工控环境下,只有能输出一个开关信号,才算有作用。这个开关信号可以利用单片机带一个继电器,用串口通讯实现,但是现场基本不会用单片机,PLC才是主流选择。
将报警信号输出至PLC,可以作为一个内部信号使用,也可以连接警报灯,蜂鸣器之类的设备,甚至可以连接至消防喷淋设备。总之比光显示在屏幕上要有意义。
变量定义:private string lastHash;//上一张图的指纹
private string nowHash;//最新一张图的指纹
private Image nowImg;//当前图像
private Plc plc;//PLC
lastHash = nowHash;
nowImg = JpgtoBuffer();
Image smallOne = smallPic(nowImg);//缩小图像
Byte[] grayValues = ToGray(smallOne);//去色
Byte Grayavg = GrayAvg(grayValues);//求灰度平均值
nowHash = GrayHash(grayValues, Grayavg);//按位对比灰度值
int diffCount = CountSameNum(nowHash, lastHash);
int diffpct = diffCount * 100 / (int)Math.Pow(32, 2);
if (diffpct >= AlarmValue.Value)
{
if (plc != null && plc.IsConnected)
plc.WriteBit(DataType.Memory, 0, 3, 0, true);
}
else
{
calc.BackColor = SystemColors.Control;
if (plc != null && plc.IsConnected)
plc.WriteBit(DataType.Memory, 0, 3, 0, false);
}
C#连接PLC使用了S7netplus,之前有写过,直接拿来用就好。上面的例子在产生报警时直接修改M3.0的状态。
你就是万金油 ̄﹃ ̄
突然高产
可能因为比较闲
感谢,下下个项目需要用到摄像头!!