因为工作需要,我一个学电气的终于要涉足图像识别领域了吗?

并不,只是做了一个简单的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;
}

抓图使用了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;
}

根据需求,可以修改缩放后的尺寸,检测对象比较明显的话,甚至可以将尺寸调整为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;
}

这种传统计算灰度的方法在计算每一个像素前都要读取一次(.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;
}

矩阵让我回想起大学时代的线性代数,一想到头就开始疼了万恶的高等数学。不过这个方法的执行速度简直起飞,不得不说一句真香。

计算指纹

接下来就是获取图片的特征,况且称之为图片的指纹。逻辑是计算图片上所有像素的灰度平均值,然后每个像素一位一位与平均值对比,如果高于平均就记"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);
}

然后按位对比:

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;
}

从获取到第二个指纹开始,就可以进行对比了:

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;
}

获取的数值就是变化明显的像素数,除以总像素数就是图片变化率。

输出信号

光判断出来变化在电脑上显示出来还不够,在工控环境下,只有能输出一个开关信号,才算有作用。这个开关信号可以利用单片机带一个继电器,用串口通讯实现,但是现场基本不会用单片机,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);
}

连接PLC使用了S7netplus,之前有写过,直接拿来用就好。上面的例子在产生报警时直接修改M3.0的状态。

说点什么
在"摄像机画面动态监测输出开关信号报警"已有6条评论
Loading...