本文介绍一种由普通PC充当监控服务器并支持网页查看的视频监控方案设计(由核心库Aforge.Net提供摄像头画面捕捉的功能,并且Aforge.Net部分可以用任何其它摄像头画面抓取方案来替代)。

一、项目结构

1. 窗口程序 将实时画面显示到监视器,并存入Redis作为Web数据来源

2. Redis 用于数据存储

3. Web 用于画面展现

4. 关于预警

原计划里,本文还会写画面分析和预警,文章写了一半就不耐烦了,就大略讲一下我的思路。要实现这部分功能,需要保存历史画面,然后将前一幅画面和当前画面灰值化(这样每个像素就由至少3B的RGB数据变成只占1B的灰度数据),累计每个像素点的差值(需要设置一个阈值,排除环境或者摄像头本身产生的干扰),即为每张图片的预警分数。以上方式只是我作为一个没有学过相关图形处理的业余人士的土办法,如果有专业的或者更简便的办法,还请留言指教。

二、窗口程序(如果不需要监控中心,这部分可以做成控制台程序)

1. 添加NuGet引用:

  • Newtonsoft.Json
  • StachExchange.Redis
  • Aforge
  • Aforge.Video
  • Aforge.Video.DirectShow

2. 业务流程

  • 向窗口添加一个flowLayoutPanel作为容器;
  • 通过new FilterInfoCollection(FilterCategory.VideoInputDevice)获取当前系统所有的图像设备(FilterInfo)的集合;
  • 遍历每个FilterInfo的时候,可以从MonikerString属性获取到设备路径;
  • 用MonikerString实例化一个VideoCaptureDevice,就能得到一个可操作的捕获设备;
  • 为每个设备新建一个PictureBox,并放入flowLayoutPanel作为监视器使用;
  • 将设备列表存入Redis;
  • 当触发捕获设备的NewFrame事件时,通过事件参数的Frame属性获取最新画面;
  • 用本设备对应的监视器加载新的画面(用于监控台),同时将新画面存入Redis(用于Web展示)。

3. 最终效果


using AForge.Video;
using AForge.Video.DirectShow;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Forms;

namespace MonitoringWinform
{
    public partial class Form1 : Form
    {
        Dictionary<string, (PictureBox monitor, FilterInfo camera, VideoCaptureDevice capture)> CamMaps =
            new Dictionary<string, (PictureBox monitor, FilterInfo camera, VideoCaptureDevice capture)>();
        ConnectionMultiplexer RedisClient = ConnectionMultiplexer.Connect("192.168.2.50:6379,password=9d8a121ce581499d");
        public Form1()
        {
            InitializeComponent();
            ReloadCams();
        }
        void ReloadCams()
        {
            EmptyList();
            FilterInfoCollection cams = 
                new FilterInfoCollection(FilterCategory.VideoInputDevice);
            string debug =
                string.Join(
                    "\r\n\r\n", 
                    cams.Cast<FilterInfo>().Select(c => $"【name】{c.Name}\r\n【path】{c.MonikerString}"));
            Dictionary<string, string> devices = new Dictionary<string, string>();
            foreach (FilterInfo cam in
                new FilterInfoCollection(FilterCategory.VideoInputDevice))
            {
                //初始化捕获设备,设置分辨率
                VideoCaptureDevice capture = new VideoCaptureDevice(cam.MonikerString);
                capture.VideoResolution = capture.VideoCapabilities[0];
                foreach (VideoCapabilities vcp in capture.VideoCapabilities)
                {
                    if (vcp.FrameSize == new Size(320, 240))
                    {                        
                        capture.VideoResolution = vcp;
                        break;
                    }
                }

                int width = capture.VideoResolution.FrameSize.Width;
                int height = capture.VideoResolution.FrameSize.Height;
                string deviceKey = ShortMd5(cam.MonikerString);
                PictureBox monitor = new PictureBox()
                {//picturebox作为监视器,查看实时画面
                    Width = width,
                    Height = height,
                    BorderStyle = BorderStyle.FixedSingle
                };
                CamMaps.Add(deviceKey, (monitor, cam, capture));
                devices.Add(deviceKey, cam.Name);
                capture.NewFrame += (object obj, NewFrameEventArgs args) =>
                {
                    //新frame显示到监视器
                    //添加到redis
                    MemoryStream mms = new MemoryStream();
                    args.Frame.Save(mms, System.Drawing.Imaging.ImageFormat.Jpeg);
                    byte[] img = mms.ToArray();
                    RedisClient.GetDatabase(1).StringSet(deviceKey, img);
                    monitor.Image = Image.FromStream(mms);
                };
                toolTip1.SetToolTip(monitor,
                    $"{cam.Name}\r\n{width} X {height}");
                flowLayoutPanel1.Controls.Add(monitor);
                capture.Start();
            }
            RedisClient.GetDatabase(1).StringSet("devices", JsonConvert.SerializeObject(devices));
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            //退出前需要停用捕获设备,解除占用,释放资源
            EmptyList();
            Environment.Exit(0);
        }
        void EmptyList()
        {
            foreach (var v in CamMaps.Values)
            {
                v.monitor.Image = null;
                v.capture.Stop();
            }
            flowLayoutPanel1.Controls.Clear();
        }
        /// <summary>
        /// 用设备路径取做MD5,作为字典的Key
        /// </summary>
        /// <param name="plain"></param>
        /// <returns></returns>
        string ShortMd5(string plain) =>
            BitConverter.ToString(
                ((HashAlgorithm)CryptoConfig.CreateFromName("MD5")).
                ComputeHash(Encoding.UTF8.GetBytes(plain))).
            Replace("-", "").Substring(8, 16).ToLower();
    }
}

三、WEB端

1. 添加引用:

  • Newtonsoft.Json
  • StachExchange.Redis

2. WEB端要做的事其实很简单:

  • 从Redis里读设备列表
  • 添加一个Action,专门用于输出指定id的设备存在Redis中的图像
# HomeController
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Web.Mvc;

namespace MonitoringWeb.Controllers
{
    public static class Sources
    {
        public static ConnectionMultiplexer RedisClient =
            ConnectionMultiplexer.Connect("192.168.2.50:6379,password=9d8a121ce581499d");
        public static Dictionary<string, string> devices =
            JsonConvert.DeserializeObject<Dictionary<string, string>>(RedisClient.GetDatabase(1).StringGet("devices"));
    }
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Cameras = Sources.devices;
            return View();
        }
        public ActionResult Live(string id = "")
        {
            Response.ClearContent();
            Response.BufferOutput = true;
            Response.ContentType = "multipart/x-mixed-replace; boundary=XSPLIT";
            Response.Headers.Add("Connection", "keep-alive");
            while (true)
            {
                try
                {
                    byte[] img = Sources.RedisClient.GetDatabase(1).StringGet(id);
                    Response.Write("--XSPLIT\r\nContent-Type:image/jpeg\r\nContent-Length:" + img.Length + "\r\n\r\n");
                    Response.BinaryWrite(img);
                }
                catch (Exception) { }
                try { Response.Flush(); } catch (Exception) { Response.End(); }
                System.Threading.Thread.Sleep(300);
            }
        }
    }
}

# /Views/Home/Index.cshtml
@{
    Dictionary<string, string> cameras =
        ViewBag.Cameras as Dictionary<string, string>;
}
<h2>Live</h2>
<div class="row">
    @foreach (var kv in cameras)
    {
        <div class="col-md-4 col-xs-6">
            <div class="panel panel-default">
                <div class="panel-heading">@kv.Value</div>
                <div class="panel-body">
                    <img src="/Home/Live/@kv.Key" width="320" height="240" />
                </div>
            </div>
        </div>
    }
</div>

3. 部署(懒得重新部署了,直接IDE里调试,然后用nginx反向代理出去)

        location / {
            proxy_pass http://localhost:2788/;
        }

PC浏览器

移动访问