这篇随笔记录一下最近做人脸识别遇到过的一些问题的解决办法,方便加强印象。以双目摄像头为例。
首先是封装:
视频处理在主窗体下操作比较简单,但是如果是将视频处理过程封装起来,就稍微有些麻烦了,会引起跨线程的问题。
首先将视频控件的对象或者是句柄传到接口里,然后对该对象进行操作。
1 private AForge.Controls.VideoSourcePlayer VideoPlayer { get; set; } 2 private AForge.Controls.VideoSourcePlayer VideoPlayer2 { get; set; } 3 public void SetAFVideo(bool isDefault, AForge.Controls.VideoSourcePlayer video) 6 { 7 if (isDefault) 8 { 9 this.VideoPlayer = video; 10 this.VideoPlayer.Paint += VideoPlayer_Paint; 11 } 12 else 13 { 14 this.VideoPlayer2 = video; 15 } 16 }
有了视频控件的对象后,接着是打开摄像头
1 /// <summary> 2 /// 打开摄像头 3 /// </summary> 4 /// <param name="isDefault"></param> 5 /// <param name="videoName">摄像头名称</param> 6 /// <returns></returns> 7 public bool AFOpenVideo(bool isDefault, string videoName) 8 { 9 FaceResult.Reset(); 10 _isStopDetect = false; 11 if (isDefault) 12 { 13 if (this.VideoPlayer == null) return false; 14 this.VideoPlayer.Show(); 15 if (_deviceVideoClr == null) 16 { 17 _deviceVideoClr = Video.GetVideoSource(videoName); 18 } 19 this.VideoPlayer.VideoSource = _deviceVideoClr; 20 this.VideoPlayer.Start(); 21 return true; 22 } 23 else 24 { 25 if (this.VideoPlayer2 == null) return false; 26 this.VideoPlayer2.Show(); 27 if (_deviceVideoGry == null) 28 { 29 _deviceVideoGry = Video.GetVideoSource(videoName); 30 } 31 this.VideoPlayer2.VideoSource = _deviceVideoGry; 32 this.VideoPlayer2.Start(); 33 return true; 34 } 35 }
关闭摄像头。关闭摄像头,有两种方式,一种是在主线程中直接关闭,另外一种是在子线程里关闭
/// <summary> /// 关闭摄像头 /// </summary> public void CloseVideo() { _isStopDetect = true; Thread.Sleep(150); } /// <summary> /// 关闭摄像头 /// </summary> public void CloseVideoDirect() { CancelFaceDetect(); if (this.VideoPlayer != null) { if (this.VideoPlayer.IsRunning) { VideoPlayer.SignalToStop(); VideoPlayer.Hide(); } } if (this.VideoPlayer2 != null) { if (this.VideoPlayer2.IsRunning) { VideoPlayer2.SignalToStop(); VideoPlayer2.Hide(); } } }
取消人脸比对
private void CancelFaceDetect() { _mEvent.Set(); }
人脸识别算法初始化
/// <summary> /// 人脸算法初始化 /// </summary> /// <returns></returns> public void InitFaceEngine(Action<bool, string> action) { //初始化过程 }
人脸比对过程,在子线程里处理,防止页面卡顿,同时每次只处理一帧。通过ManualResetEvent信号变量来终止人脸比对过程,视频比对过程在
FaceDetect方法里。比对完成,必须释放img对象占用的内存
1 public void StartFaceDetect() 2 { 3 if (!_isFaceCompare) return; 4 _mEvent.Reset(); 5 Task.Factory.StartNew(() => 6 { 7 Task.Delay(2000).Wait(); 8 while (true) 9 { 10 try 11 { 12 if (_mEvent.WaitOne(100)) 13 { 14 FaceResult.Reset(); 15 break; 16 } 17 var imgClr = this.VideoPlayer.GetCurrentVideoFrame(); 18 var imgGry = this.VideoPlayer2.GetCurrentVideoFrame(); 19 float offsetX = VideoPlayer.Width * 1f / imgClr.Width; 20 float offsetY = VideoPlayer.Height * 1f / imgClr.Height; 21 FaceResult.OffsetX = offsetX; 22 FaceResult.OffsetY = offsetY; 23 FaceDetect(imgClr, imgGry); 24 imgClr.Dispose(); 25 imgGry.Dispose(); 26 } 27 catch (System.Exception ex) 28 { 29 throw new Exception(ex.Message); 30 } 31 } 32 }); 33 }
给人脸框选。通过_isStopDetect变量来标识,在人脸识别通过后是否关闭摄像头
1 private void VideoPlayer_Paint(object sender, System.Windows.Forms.PaintEventArgs e) 2 { 3 for (int i = 0; i < FaceResult.FaceNumber; i++) 4 { 5 //根据Rect进行画框 6 e.Graphics.DrawRectangle(_rectPen, FaceResult.X, FaceResult.Y, FaceResult.Width, FaceResult.Height); 7 if (_trackUnit.message != "" && FaceResult.X > 0 && FaceResult.Y > 0) 8 { 9 //将上一帧检测结果显示到页面上 10 e.Graphics.DrawString(_trackUnit.message, _trackFont, _trackBrush, FaceResult.X, FaceResult.Y + 5); 11 } 12 } 13 if (_isStopDetect) 14 { 15 CloseVideoDirect(); 16 } 17 }
将比对结果实时告诉前端。比对的信息通过事件FaceDetectShowHandler 来通知到前端,人脸比对成功通过事件FaceDectctResultHandler通知前端,前端收到FaceDectctResultHandler后执CloseVideo(),以关闭摄像头
1 public Action<string> FaceDetectShowHandler { get; set; } 2 public Action FaceDectctResultHandler { get; set; } 3 /// <summary> 4 /// 人脸比对 5 /// </summary> 6 /// <param name="bitClr"></param> 7 /// <param name="bitGry"></param> 8 public void FaceDetect(Bitmap bitClr, Bitmap bitGry) 9 { 10 //... 11 this.__errString = "没有检测到人脸,请正视相机!!"; 12 FaceDetectShowHandler?.Invoke(_errString); 13 this.__errString = "相似度高"; 14 FaceDectctResultHandler?.Invoke(); 15 16 }
通过前面的示例代码知道,视频比对是在一个task子线程进行,前端要收到比对信息的消息通知,也是在子线程进行,如果将消息显示在前端界面上进行刷新,就必须通过委托
来刷新界面,否则引起跨线程访问的问题。同理,前端收到FaceDectctResultHandler事件通知后,要关闭摄像头,就不能直接调用close方法来关闭,而必须在主线程
关闭,这里通过一个变量_isStopDetect在paint事件里关闭摄像头,因为Paint事件是主线程,所以主线程里关闭摄像头完全没问题。另外关于视频图像的处理,有两种
方式,一种是用一个while循环,一种是通过线程池,每捕捉到一帧图像就丢到线程池里异步处理,但是用线程池可能会引发一个问题,就是引用类型变量在一个循环没有处
理完时,变量就被修改了,此时容易引起程序崩溃。用while循环,需要开启一个子线程,防止页面卡顿,是一个同步处理的过程。
在关闭摄像头时,有一个休眠150毫秒的动作,目的是让ManualResetEvent先收到终止的信号,终止while循环,然后执行关闭摄像头的动作,否则时间过短,导致while
一直执行。