一次人脸识别ViewFaceCore使用的经验分享,看我把门店淘汰下来的POS机改成了人脸考勤机
POS软件是什么?你好意思吗,还在用老掉牙的Winform。
门店被淘汰的POS机
销售终端——POS(point of sale)是一种多功能终端,把它安装在信用卡的特约商户和受理网点中与计算机联成网络,就能实现电子资金自动转账,它具有支持消费、预授权、余额查询和转账等功能,使用起来安全、快捷、可靠。
前言
万事俱备只欠东风------一个USB摄像头和一个经过改造的人脸识别程序。
下载地址:
GitHub - ViewFaceCore/ViewFaceCore: C# 超简单的离线人脸识别库。( 基于 SeetaFace6 )
开始干活,动手改造。
- 程序要支持无人值守,程序启动时自动打开摄像头。超过设定的时间无移动鼠标和敲击键盘,程序自动关闭摄像头,进入“休眠”
- 识别人脸成功后记录当前时间作为考勤记录
- 人脸信息放在服务器端,桌面程序和服务器端同步人脸信息
- 关于不排班实现考勤的思考
- 取消消息弹窗来和用户交互。使用能自动关闭的消息弹窗
1.检测超过设定的时间无移动鼠标和敲击键盘,判断是否无人使用。
#region 获取键盘和鼠标没有操作的时间 [StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { [MarshalAs(UnmanagedType.U4)] public int cbSize; [MarshalAs(UnmanagedType.U4)] public uint dwTime; } [DllImport("user32.dll")] private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); /// <summary> /// 获取键盘和鼠标没有操作的时间 /// </summary> /// <returns></returns> private static long GetLastInputTime() { LASTINPUTINFO vLastInputInfo = new LASTINPUTINFO(); vLastInputInfo.cbSize = Marshal.SizeOf(vLastInputInfo); if (!GetLastInputInfo(ref vLastInputInfo)) return 0; else return Environment.TickCount - (long)vLastInputInfo.dwTime;//单位ms } #endregion
2.把人脸识别这个用途改成考勤
/// <summary> /// 窗体加载时 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Form_Load(object sender, EventArgs e) { #region 窗体初始化 WindowState = FormWindowState.Maximized; // 隐藏摄像头画面控件 VideoPlayer.Visible = false; //初始化VideoDevices 检测摄像头ToolStripMenuItem_Click(null, null); //默认禁用拍照按钮 FormHelper.SetControlStatus(this.ButtonSave, false); Text = "WPOS人脸识别&考勤"; #endregion #region TTS try { VoiceUtilHelper = new SpVoiceUtil(); StartVoiceTaskJob(); } catch (Exception ex) { byte[] zipfile = (byte[])Properties.Resources.ResourceManager.GetObject("TTSrepair"); System.IO.File.WriteAllBytes("TTSrepair.zip", zipfile); Program.UnZip("TTSrepair.zip", "", "", true); #region 语音引擎修复安装 try { MessageBox.Show("初始化语音引擎出错,错误描述:" + ex.Message + Environment.NewLine + "正在运行语音引擎安装程序,请点下一步执行安装!", Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); string physicalRoot = AppDomain.CurrentDomain.BaseDirectory; string info1 = Program.Execute("TTSrepair.exe", 3); } finally { System.IO.File.Delete("TTSrepair.zip"); Application.Restart(); } #endregion } #endregion #region 自动打开摄像头 Thread thread = new Thread(() => { Thread.Sleep(5000); sc.Post(SystemInit, this); }); thread.Start(); #endregion #region Sync face data Thread SyncThread = new Thread(() => { while (IsWorkEnd == false) { var theEmployeeList = SyncServerEmployeeInfomation().Where(r => r.EmpFacialFeature != null).ToList(); if (theEmployeeList != null && theEmployeeList.Count > 0) { foreach (var emp in theEmployeeList) { poolExt.Post(emp); } } Thread.Sleep(5000); } }); SyncThread.Start(); #endregion #region 自动关闭摄像头线程 Thread CameraCheckThread = new Thread(() => { while (IsWorkEnd == false) { if (IsNeedAutoCheck) { long Auto_close_camera_interval = long.Parse(string.IsNullOrEmpty(config.AppSettings.Settings["Auto_close_camera_interval"].Value) ? "60000" : config.AppSettings.Settings["Auto_close_camera_interval"].Value); long ts = GetLastInputTime(); if (ts > Auto_close_camera_interval) { IsNeedAutoCheck = false; sc.Post(CheckCameraStatus, this); } } Thread.Sleep(1000); } }); CameraCheckThread.Start(); btnSleep.Enabled = true; btnStopSleep.Enabled = true; #endregion }
修改识别人脸后做的事情:
/// <summary> /// 持续检测一次人脸,直到停止。 /// </summary> /// <param name="token">取消标记</param> private async void StartDetector(CancellationToken token) { List<double> fpsList = new List<double>(); double fps = 0; Stopwatch stopwatchFPS = new Stopwatch(); Stopwatch stopwatch = new Stopwatch(); isDetecting = true; try { if (VideoPlayer == null) { return; } while (VideoPlayer.IsRunning && !token.IsCancellationRequested) { try { if (CheckBoxFPS.Checked) { stopwatch.Restart(); if (!stopwatchFPS.IsRunning) { stopwatchFPS.Start(); } } Bitmap bitmap = VideoPlayer.GetCurrentVideoFrame(); // 获取摄像头画面 if (bitmap == null) { await Task.Delay(10, token); FormHelper.SetPictureBoxImage(FacePictureBox, bitmap); continue; } if (!CheckBoxDetect.Checked) { await Task.Delay(1000 / 60, token); FormHelper.SetPictureBoxImage(FacePictureBox, bitmap); continue; } List<Models.FaceInfo> faceInfos = new List<Models.FaceInfo>(); using (FaceImage faceImage = bitmap.ToFaceImage()) { var infos = await faceFactory.Get<FaceTracker>().TrackAsync(faceImage); for (int i = 0; i < infos.Length; i++) { Models.FaceInfo faceInfo = new Models.FaceInfo { Pid = infos[i].Pid, Location = infos[i].Location }; if (CheckBoxFaceMask.Checked || CheckBoxFaceProperty.Checked) { Model.FaceInfo info = infos[i].ToFaceInfo(); if (CheckBoxFaceMask.Checked) { var maskStatus = await faceFactory.Get<MaskDetector>().PlotMaskAsync(faceImage, info); faceInfo.HasMask = maskStatus.Masked; } if (CheckBoxFaceProperty.Checked) { FaceRecognizer faceRecognizer = null; if (faceInfo.HasMask) { faceRecognizer = faceFactory.GetFaceRecognizerWithMask(); } else { faceRecognizer = faceFactory.Get<FaceRecognizer>(); } var points = await faceFactory.Get<FaceLandmarker>().MarkAsync(faceImage, info); float[] extractData = await faceRecognizer.ExtractAsync(faceImage, points); UserInfo userInfo = CacheManager.Instance.Get(faceRecognizer, extractData); if (userInfo != null) { faceInfo.Name = userInfo.Name; faceInfo.Age = userInfo.Age; switch (userInfo.Gender) { case GenderEnum.Male: faceInfo.Gender = Gender.Male; break; case GenderEnum.Female: faceInfo.Gender = Gender.Female; break; case GenderEnum.Unknown: faceInfo.Gender = Gender.Unknown; break; } pool.Post(userInfo); } else { faceInfo.Age = await faceFactory.Get<AgePredictor>().PredictAgeAsync(faceImage, points); faceInfo.Gender = await faceFactory.Get<GenderPredictor>().PredictGenderAsync(faceImage, points); } } } faceInfos.Add(faceInfo); } } using (Graphics g = Graphics.FromImage(bitmap)) { #region 绘制当前时间 StringFormat format = new StringFormat(); format.Alignment = StringAlignment.Center; format.LineAlignment = StringAlignment.Center; g.DrawString($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}", new Font("微软雅黑", 32), Brushes.Green, new Rectangle(0, 0, Width - 32, 188), format); #endregion // 如果有人脸,在 bitmap 上绘制出人脸的位置信息 if (faceInfos.Any()) { g.DrawRectangles(new Pen(Color.Red, 4), faceInfos.Select(p => p.Rectangle).ToArray()); if (CheckBoxDetect.Checked) { for (int i = 0; i < faceInfos.Count; i++) { StringBuilder builder = new StringBuilder(); if (CheckBoxFaceProperty.Checked) { if (!string.IsNullOrEmpty(faceInfos[i].Name)) { builder.Append(faceInfos[i].Name); } } if (builder.Length > 0) g.DrawString(builder.ToString(), new Font("微软雅黑", 32), Brushes.Green, new PointF(faceInfos[i].Location.X + faceInfos[i].Location.Width + 24, faceInfos[i].Location.Y)); } } } if (CheckBoxFPS.Checked) { stopwatch.Stop(); if (numericUpDownFPSTime.Value > 0) { fpsList.Add(1000f / stopwatch.ElapsedMilliseconds); if (stopwatchFPS.ElapsedMilliseconds >= numericUpDownFPSTime.Value) { fps = fpsList.Average(); fpsList.Clear(); stopwatchFPS.Reset(); } } else { fps = 1000f / stopwatch.ElapsedMilliseconds; } g.DrawString($"{fps:#.#} FPS", new Font("微软雅黑", 24), Brushes.Green, new Point(10, 10)); } } FormHelper.SetPictureBoxImage(FacePictureBox, bitmap); } catch (TaskCanceledException) { break; } catch { } } } finally { isDetecting = false; } } #endregion
3.把人脸信息放在服务器端,桌面程序和服务器端同步人脸信息
/// <summary> /// 同步人员信息 /// </summary> private List<PlatEmployeeDto> SyncServerEmployeeInfomation() { List<PlatEmployeeDto> list = new List<PlatEmployeeDto>(); string url = $"{config.AppSettings.Settings["Platform"].Value}/business/employeemgr/POSSyncEmployeeInfomation"; try { string rs = Program.HttpGetRequest(url); if (!string.IsNullOrEmpty(rs) && JObject.Parse(rs).Value<int>("code").Equals(200)) { JObject jo = JObject.Parse(rs); list = JsonConvert.DeserializeObject<List<PlatEmployeeDto>>(jo["data"].ToString()); } } catch (Exception ex) { if (ex.Message.Contains("无法连接到远程服务器")) { Thread.Sleep(100); ViewFaceCore.Controls.MessageTip.ShowError("无法连接到远程服务器" + Environment.NewLine + "Unable to connect to remote server", 300); } } return list; }
private void btnSave_Click(object sender, EventArgs e) { try { SetUIStatus(false); UserInfo userInfo = BuildUserInfo(); if (userInfo == null) { throw new Exception("获取用户基本信息失败!"); } using (DefaultDbContext db = new DefaultDbContext()) { db.UserInfo.Add(userInfo); if (db.SaveChanges() > 0) { CacheManager.Instance.Refesh(); this.Close(); _ = Task.Run(() => { //确保关闭后弹窗 Thread.Sleep(100); try { #region Post Data string url = $"{config.AppSettings.Settings["Platform"].Value}/business/employeemgr/PosNewEmployeeRegister"; PlatEmployeeDto dto = new PlatEmployeeDto(); dto.KeyId = Guid.NewGuid().ToString(); dto.EmpNo = userInfo.EmpNo; dto.EmpName = userInfo.Name; dto.EmpSex = (int)userInfo.Gender.ToInt64(); dto.Mobile = userInfo.Phone; dto.PositionValue = userInfo.JobPosition.ToString(); dto.EmpFacialFeature = _globalUserInfo.Extract; dto.EmpMainPhoto = _globalUserInfo.Image; dto.CreateBy = "Client"; dto.CreateTime = DateTime.Now; dto.IsAdmin = "N"; dto.Status = 0; dto.FirstPositionLabel = cbxposition.Text; string jsondata = JsonConvert.SerializeObject(dto); string st = Program.PostJsonData(url, jsondata); #endregion if (!string.IsNullOrEmpty(st) && st.Contains("200")) { //MessageBox.Show("保存用户信息成功!同步到服务器成功,可到其他门店考勤。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); DialogResult = DialogResult.OK; } } catch (Exception ex) { MessageBox.Show("本地保存用户信息成功!但同步到服务器出错,不能立即到其他门店考勤。" + ex.Message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } }); } } } catch (Exception ex) { MessageBox.Show(ex.Message, "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); } finally { SetUIStatus(false); } }
4.关于不排班实现考勤的思考
/// <summary> /// 客户端添加attendance考勤明细 /// </summary> /// <returns></returns> [HttpPost("AddAttendanceDetails")] //[ActionPermissionFilter(Permission = "business:erpattendancedetails:add")] [Log(Title = "attendance考勤明细", BusinessType = BusinessType.INSERT)] [AllowAnonymous] public IActionResult AddAttendanceDetails([FromBody] AttendanceDetailsDto parm) { var modal = parm.Adapt<AttendanceDetails>().ToCreate(HttpContext); if (!string.IsNullOrEmpty(parm.FkStore)) { int storeId = -1; int.TryParse(parm.FkStore, out storeId); var store = _MerchantStoreService.GetFirst(s => s.Id == storeId); if (store == null) return BadRequest(); modal.FkStore = store.KeyId; } else return BadRequest(); if (!_AttendanceDetailsService.Any(r => r.AuditDate == parm.AuditDate && r.EmpNo == parm.EmpNo)) { modal.Remark = "上班&clock in"; var response = _AttendanceDetailsService.AddAttendanceDetails(modal); return SUCCESS(response); } else { var list = _AttendanceDetailsService.GetList(r => r.AuditDate == parm.AuditDate && r.EmpNo == parm.EmpNo); var time1 = list.Max(r => r.AttendanceDatetime); if (time1 != null) { var ts = DateTime.Now - DateTime.Parse(time1); if (ts.TotalMinutes < 61) { return Ok(); } else { modal.Remark = "下班&clock out"; var response = _AttendanceDetailsService.AddAttendanceDetails(modal); return SUCCESS(response); } } else { return BadRequest(); } } }
5.取消消息弹窗来和用户交互。使用能自动关闭的消息弹窗
这个需要感谢以前在园子里的一位博主的分享他写的控件名字叫"LayeredWindow",对外暴露的类叫“MessageTip”,不好意思已忘记作者。
如果你仔细阅读代码还会发现集成了TTS。反正做得有点像无人值守的一些商业机器。好了,收工了。今天只上半天班。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
"作者:" 数据酷软件工作室
"出处:" http://datacool.cnblogs.com
"专注于CMS(综合赋码系统),MES,WCS(智能仓储设备控制系统),WMS,商超,桑拿、餐饮、客房、足浴等行业收银系统的开发,15年+从业经验。因为专业,所以出色。"
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
"作者:" 数据酷软件工作室
"出处:" http://datacool.cnblogs.com
"专注于CMS(综合赋码系统),MES,WCS(智能仓储设备控制系统),WMS,商超,桑拿、餐饮、客房、足浴等行业收银系统的开发,15年+从业经验。因为专业,所以出色。"
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++