基于SeetaFace+VS2017+Qt的人脸识别

1 实验目的

    目前计算机视觉技术已经比较成熟,相关的开源项目与算法很多,可以将这些开源算法进行整合,进而做成一个小项目,以供日后学习与研究。本实验主要将利用人脸识别开源项目SeetaFace,结合使用OpenCV工具,结合VS2017与Qt实现一个人脸识别的小项目。最后对实验系统进行测试评估。

2 相关知识与技术介绍

2.1 OpenCV简介

    OpenCV(Open Source Computer Vision Library),是一个开源的可以跨平台运行的计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效,由一系列C函数和少量C++类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。

    OpenCV的设计理念是所包含的函数能以最快的速度进行编译,之所以使用C代码进行编写,就是希望能够利用多核处理器的优势达到最快的运行速度。它构建了一个方便开发人员使用的、简单易懂的计算机视觉框架,在这个基础上,开发人员能都更方便的设计出更复杂的计算机视觉相关程序。OpenCV是由Intel发起的项目,其中的源代码都是开源免费的代码,因此可以用于科研人员的研究领域,也可以用于商业领域。最新的版本OpenCV3.4.2已于2018年7月4日发布。

2.2 SeetaFace简介

    SeetaFace是由中科院山世光老师带领的人脸识别研发组基于C++代码研发的人脸识别算法。SeetaFace人脸识别引擎包括了搭建一套全自动人脸识别系统所需的三个核心模块,即:人脸检测模块SeetaFace Detection、面部特征点定位模块SeetaFace Alignment以及人脸特征提取与比对模块SeetaFace Identification。

    其中,SeetaFace Detection采用了一种结合传统人造特征与多层感知机(MLP)的级联结构,在FDDB上达到了84.4%的召回率(100个误检时),并可在单个i7 CPU上实时处理VGA分辨率的图像。

    面部特征点定位模块SeetaFace Alignment通过级联多个深度模型(栈式自编码网络)来回归5个关键特征点(两眼中心、鼻尖和两个嘴角)的位置,在AFLW数据库上达到state-of-the-art的精度,定位速度在单个i7 CPU上超过200fps。

    人脸识别模块SeetaFace Identification采用一个9层的卷积神经网络(CNN)来提取人脸特征,在LFW数据库上达到97.1%的精度(注:采用SeetaFace人脸检测和SeetaFace面部特征点定位作为前端进行全自动识别的情况下),特征提取速度为每图120ms(在单个i7 CPU上)。


3 基于SeetaFace的人脸识别算法

    SeetaFace人脸识别引擎包括三个核心模块,即:人脸检测模块SeetaFace Detection、面部特征点定位模块SeetaFace Alignment以及人脸特征提取与比对模块SeetaFace Identification。下面对上述三个模块的情况做简要介绍。

3.1 人脸检测模块SeetaFace Detection

    该模块基于一种结合经典级联结构和多层神经网络的人脸检测方法实现,其所采用的漏斗型级联结构(Funnel-Structured Cascade,FuSt)专门针对多姿态人脸检测而设计,其中引入了由粗到精的设计理念,兼顾了速度和精度的平衡。如图1所示,FuSt级联结构在顶部由多个针对不同姿态的快速LAB级联分类器构成,紧接着是若干个基于SURF特征的多层感知机(MLP)级联结构,最后由一个统一的MLP级联结构(同样基于SURF特征)来处理所有姿态的候选窗口,整体上呈现出上宽下窄的漏斗形状。

    从上往下,各个层次上的分类器及其所采用的特征逐步变得复杂,从而可以保留人脸窗口并排除越来越难与人脸区分的非人脸候选窗口。 


图1  SeetaFace人脸检测模块所采用的FuSt漏斗型级联结构

    整个算法采用漏斗型,先采用计算量小的特征,快速过滤大量非人脸窗口(图像滑窗),然后采用复杂结构逐层筛选人脸。由图2所示SeetaFace检测效果图看到在人脸局部遮挡的情况下也能很好的检测到人脸区域。

图2 SeetaFace检测效果图

3.2 面部特征点定位模块SeetaFace Alignment

    面部特征点定位(人脸对齐)在人脸识别、表情识别、人脸动画合成等诸多人脸分析任务中扮演着非常重要的角色。由于姿态、表情、光照和遮挡等因素的影响,真实场景下的人脸对齐任务是一个非常困难的问题。形式上,该问题可以看作是从人脸表观到人脸形状的复杂非线性映射。为此,SeetaFace Alignment采用一种由粗到精的自编码器网络(Coarse-to-Fine Auto-encoder Networks, CFAN)来求解这个复杂的非线性映射过程。如图3所示,CFAN级联了多级栈式自编码器网络,其中的每一级都刻画从人脸表观到人脸形状的部分非线性映射。具体来说,输入一个人脸区域(由人脸检测模块得到),第一级自编码器网络直接从该人脸的低分辨率版本中快速估计大致的人脸形状S0。然后,提高输入人脸图像的分辨率,并抽取当前人脸形状S0(相应提升分辨率)各特征点位置的局部特征,输入到下一级自编码器网络来进一步优化人脸对齐结果。以此类推,通过级联多个栈式自编码器网络,在越来越高分辨率的人脸图像上逐步优化人脸对齐结果。


图3人脸对齐方法

    此次开源的SeetaFace Alignment基于上述CFAN方法实现了5个面部关键特征点(两眼中心,鼻尖和两个嘴角)的精确定位,训练集包括23,000余幅人脸图像(标注了5点)。需要注意的是,为加速之目的,在基本不损失精度的情况下,开源实现中将CFAN级联的数目减少到了2级,从而可在单颗Intel i7-3770 (3.4 GHz CPU)上达到每个人脸5ms的处理速度(不包括人脸检测时间)。

图4是本文通过SeetaFace Alignment分别对不同人员在不同状态特征点定位得到的效果图。测试中发现通过多次验证,在不同表情、不同面部偏转角度、抬头低头、脸部局部遮挡等情况下都能有效的定位。且对于戴眼镜测试者,也能很好的定位到特征区域。

图4 SeetaFace人脸对齐

3.3人脸特征提取与比对模块SeetaFace Identification

    人脸识别本质上是要计算两幅图像中人脸的相似程度,其一为注册阶段(类比人的相识过程)输入系统的,另一幅为识别阶段(即再见时的辨认过程)的输入。为此,如图5所示,一套全自动的人脸识别系统在完成前述的人脸检测与人脸对齐两个步骤之后,即进入第三个核心步骤:人脸特征提取和比对。这个阶段也是深度学习风起云涌之后进步最大的模块,目前大多数优秀的人脸识别算法均采用卷积神经网络(CNN)来学习特征提取器(即图5中的函数F)。

图5 人脸识别系统的核心流程

    SeetaFace开源的人脸特征提取模块也是基于卷积神经网络的。具体地说,是深度卷积神经网络VIPLFaceNet:一个包含7个卷积层与2个全连接层的DCNN。其直接修改自Hinton教授的学生Alex Krizhevsky等于2012年设计的AlexNet(即引爆CNN在视觉中广泛应用的网络)。

    与开源的SeetaFace Identification代码一起发布的人脸识别模型是使用140万人脸图像训练出来的,这些训练图像来自于约1.6万人,其中既有东方人也有西方人。人脸特征直接采用VIPLFaceNet FC2层的2048个结点的输出,特征比对可简单采用Cosine计算相似度,然后进行阈值比较(验证应用)或排序(识别应用)即可。

    在LFW standard Image-Restricted测试协议下,使用SeetaFace Detection与SeetaFace Alignment检测并对齐人脸,采用SeetaFace Identification进行特征提取和比对,可以达到97.1%的识别正确率(请注意:这是系统全自动运行的结果,对少量不能检到人脸的图像,截取中间区域输入人脸对齐模块即可)。速度方面,在单颗Intel i7-3770 CPU上,开源代码提取一张人脸之特征的时间约为120ms(不含人脸检测和特征点定位时间)。

    以上关于SeetaFace的理论介绍基本来源于SeetaFace官方的说明,想详细了解SeetaFace更多的理论信息,请参考文章:SeetaFace开源人脸识别引擎介绍,该文章底部的参考文献附有论文,有想深入研究的可以去啃啃论文(全英文的0_0)。

    上面关于理论的确实介绍的有点多了,不过了解一下,可以增加一下自己的眼界。废话不多说了,下面进入正题-应用部分。

4 系统环境搭建与实验结果

通过对上述的理论知识的了解学习,接下来进行系统的环境搭建,然后进行具体的实验。然后对实验结果做一定的分析。

4.1系统的总体设计方案

首先使用摄像头读取采集的图像,经过人脸检测模块框出人脸,然后使用面部特征点定位(人脸对齐)模块对5个面部关键特征点(两眼中心,鼻尖和两个嘴角)进行标记显示,最后使用人脸特征提取与比对模块进行人脸特征提取和比对。在系统中设置一个阈值0.7,若比对后的相似度大于0.7,就认定为同一个人并触发警报系统。系统的功能框图如图6所示。


图6 系统功能框图

4.2 配置系统环境

4.2.1 配置OpenCV

Open CV中包含很多图像处理的算法,因此学会正确使用Open CV也是人脸识别研究的一项重要工作。在 VS2017中应用Open CV,需要进行手动配置,下面给出在VS2017中配置Open CV的详细步骤。

1.下载并安装OpenCV3.4.1与VS2017的软件。

2.配置Open CV环境变量。

计算机->(右键)属性->高级系统设置->高级->环境变量->(双击)系统变量中的path->在变量只里面添加相应的路径。添加的路径为:“....opencv\build\x64\vc15\bin”。里面的省略号请换成自己电脑上的路径,例如:E:\opencv\build\x64\vc15\bin。如图7所示。


图7 配置OpenCV环境变量

3.配置工程目录与链接库

需要配置包含目录和库目录,首先打开,视图->解决方案管理器->(右键)项目->属性->VC++目录。

1)配置包含目录。

添加“...opencv\build\include;...opencv\build\include\opencv;...opencv\build

\incude\opencv2”即可,里面的省略号请换成自己电脑上的路径,如图8所示。


图8 配置包含目录

2)配置库目录

添加“E:\opencv\build\x64\vc15\lib即可,里面的省略号请换成自己电脑上的路径,如图9所示。


图9 配置库目录

3)配置链接库

首先打开,视图->解决方案管理器->(右键)项目->属性->链接器->输入->附加依赖项,

针对Debug配置添加“opencv_world341d.lib”,若在Release下,就添加“opencv_world341.

lib”。这是OpenCV3版本的方便之处,OpenCV2版本需要添加很多项。操作如图10所示。


图10 配置链接库

    上述三步配置要注意每次新建工程都要重新配置,但也可以只配置一次,达到以后都不用单独配置的效果,那么就在属性管理器->展开项目->Debug|x64(或者Release|x64)-(双击)进Microsoft.Cpp.x64.user,然后后续操作和前面三步一样。

4.2.2 配置SeetaFace

首先,在开源项目平台GitHub下载SeetaFace开源项目。

下载链接:https://github.com/seetaface/SeetaFaceEngine。下载好以后,然后根据SeetaFace开源项目中的配置说明文档进行配置。

在VS2017中新建三个dll(动态链接库)项目,将SeetaFace的三个模块FaceDetection、FaceAlignment和FaceIdentification分别制作成DLL项目,然后将生成的lib和dll文件保存下载,以便在项目中直接添加使用。在这里就不详细地进行具体配置说明了。

4.3 系统的软件实现

系统的硬件部分是基于一个720p罗技c310的USB摄像头,进行图像采集工作,所以重要的工作在软件设计方面。下面将进行具体的软件实现。

4.3.1 系统中的图像类型转换

通过OpenCV中VideoCapture类,可以进行图像的逐帧采集,采集到的图像类型为Mat类型。SeetaFace中的图像类型为ImageData类型,而且在后面的Qt平台下会使用到的图片类型为QImage。以上这三种类型在某些情况下需要进行必要的转换,下面对具体的类型转换进行说明。

1)Mat类型转ImageData

首先将Mat类型的图片转为单通道的灰度图(如果已是灰度图就不用转了),具体转化代码如下:

cv::Mat img;

cv::cvtColor(frame, img_gray, cv::COLOR_BGR2GRAY);

seeta::ImageData image;

image.data = img.data;

image.width = img.cols;

image.height = img.rows;

image.num_channels = 1;

2)Mat类型转QImage

其中需要将Mat类型的BGR通道顺序变换为QImage的RGB顺序,可以调用OpenCV中的cvtColor函数实现,以上是对两种图像类型的data部分的格式进行调整,下一步只需要明确Mat的头结构里的变量与QImage的头结构里的变量的对应关系即可实现转换,具体转换代码如下: 

Mat frame,temp;

      cvtColor(frame, temp, CV_BGR2RGB);

 QImage image = QImage((const unsigned char *)(temp.data), temp.cols,

temp.rows, QImage::Format_RGB888);

3)QImage类型转Mat

与2)的类似,将QImage的RGB通道顺序变换为Mat类型的BGR顺序,然后将QImage的头结构里的变量与Mat的头结构里的变量的对应关系即可实现转换,具体转换代码如下:

Mat frame;

cv::Mat Temp = cv::Mat(image.height(), image.width(), CV_8UC3,(void*)image.constBits(), image.bytesPerLine());

cvtColor(Temp,frame,CV_RGBA2BGR);

4.3.2 线程设计

把系统分为两个线程,即主线程与子线程。主线程主要负责UI及一些一般的处理,子线程负责人脸特征提取与比对模块SeetaFace Identification部分的数据处理,因为这块比较耗资源,如果都放在子线程的话,会是UI卡顿。

1)主线程

主线程部分,主要进行摄像头的采集图像并显示在UI上,以及人脸对齐的显示,然后就是一些其他的小功能,例如,拍照/截图功能。

下面就主线程的主要功能进行分析与说明。

因为是对摄像头采集的图像进行基本类似于实时的处理,因此在系统中设置定时器,进行定时处理,因为主线程中有UI还要工作的原因,故设置为死循环的方式不可取。在Qt中的定时器设置定时时间长度设定为35ms,理论上大概每秒能获取28帧图像,基本可以满足实时性的要求。

在获取摄像头图像的基础上,进行5个面部关键特征点(两眼中心,鼻尖和两个嘴角)的定位并显示在UI上。主体流程图如图11所示。

图11 主线程主体流程图

2)子线程

    因为系统要基于Qt实现UI,其他线程中不能操作UI对象,只有主线程能对UI进行操作,所以将人脸特征提取与比对模块SeetaFace Identification放在子线程。将SeetaFace Identification部分的处理函数设置为可控的死循环,设置bool变量来对死循环进行控制。然而,UI要将处理后的信息显示出来,因此线程之间要进行通信。

3)线程通信

​​​​​​​​​​线程的通信部分可以使用Qt的“信号与槽”功能,使用连接函数将信号与槽函数连接,将线程中需要传递的数据放在信号中,一旦信号发射,就执行相应的槽函数,来完成线程之间的通信任务。流程图如图12所示。

图12 线程通信流程图

    由图5所知,要在系统中输入一张需要进行比对的图片,在UI中通过按钮获取该图片的路径信息,然后通过信号发射这个路径,然后子线程那边接收到信号后,将这个图片路径获取下来以便进行接下来的人脸特征提取与比对。当主线程触发人脸识别的信号以后,子线程接收后将摄像头获取的图像与输入图像进行人脸特征提取与比对。最后将人脸比对的相似度及被认定为同一个人的裁剪人脸通过信号发送给主线程,主线程在UI上进行数据显示。

4.4 基于Qt的UI设计

Qt是一个跨平台的C++图形用户界面应用程序框架。它为应用程序开发者提供建立艺术级图形界面所需的所有功能。它是完全面向对象的,很容易扩展,并且允许真正的组件编程。

Qt提供了一种称为信号与槽的方式,使得各个元件之间的协同工作变得十分简单。Qt拥有自己的集成开发环境(IDE)名为Qt Creator,但为了使各个部分的衔接方便,在VS2017上安装一个叫Qt Visual Studio Tools的Qt插件。这样就可以进行VS加Qt的混合编程。

1)UI设计图

图13 UI的设计图

2)信号与槽

UI部分主要使用的是按钮来进行操作,按钮的点击信号与对应的槽函数如图14所示。具体的槽函数请参见附录中的代码。

图14 信号与槽

4.5 实验结果与分析

在完成上述的各项准备工作之后,下面就进行最后的整体调试工作。运行已经写好的程序,然后生成应用程序界面,界面样式如图15所示。采用了一些逻辑方法,去除了窗体的边框,然后再右上角设置了一个关闭按钮,这个关闭按钮具有关闭线程,退出窗口等其他功能。

图15 应用程序界面

    接下来,通过具体的实验来进行演示操作,具体操作为:打开摄像头->人脸检测与对齐->输入图片->人脸识别。可以发现SeetaFace具有很好的识别能力,在输入同一个人的照片后,进行人脸识别,可以达到0.9以上相似度(图16中的相似度为0.911083),然后显示发现目标并发出警报,可见效果还是很不错的,如图16所示。

图17 不同人的效果演示

    在实验过程中,发现一个问题,当输入一张同一个人之前的照片,识别的相似度会有所下降,会比输入现拍的照片相似度低,但也基本大于0.7(图18中的相似度为0.76884),即被判别为同一个人,如图18所示。可能这是SeetaFace算法存在的小缺陷吧,证明算法还有优化的空间。

图18 输入非现拍图片的效果演示

    基于同样的原理,该系统还具备视频人脸识别功能。打开一段视频,输入一张图片,即可实现在视频中查找到与输入图片为同一个人的人脸,然后将找到后人脸保存在应用程序的文件夹中,实验效果如图19所示(图19中的相似度为0.7762)。

图19 视频人脸识别

5 总结

    本文主要利用人脸识别开源项目SeetaFace,使用OpenCV作为图像处理工具,结合集成开发环境VS2017与C++图形用户界面应用程序框架Qt实现一个人脸识别的小项目。主要工作是首先进行系统各项环境与功能的搭建与实施,在基于开源库与项目的基础进行了应用性的实验与结果分析。

    本文首先进行了基本知识与技术的介绍,进行了相关理论的了解与学习,为接下来的系统实施打下了基础。OpenCV是图像处理常用的工具,在在学习与科研过程中具有很好的帮助性。SeetaFace作为为数不多的开源人脸识别项目,很值得学习与研究,是一个很不错的开源项目。

    接下来本文进行了系统环境的搭建与实验,然后根据实验进行了结果分析。在系统搭建环节,首先确定了系统的总体设计思路,然后进行了系统环境配置。首先配置的是OpenCV,其次进行SeetaFace的配置。在系统软件设计中先进行了系统中图像类型转换的介绍,然后进行了多线程的设计。

最后实施了具体的实验,根据实验对系统进行了分析与探讨。从实验结果发现该系统具有良好的人脸识别特性,但也有一些需要优化的空间。本文虽然基本整体实现了系统设计的功能,但在许多方面还存在着瑕疵,还有很多值得改进的地方。其实可以将该系统进行嵌入式等相关平台的移植,进一步增加该系统的应用价值。

参考文献

[1]朱兴统,习洋洋.基于C++和OpenCV的人脸识别系统的设计与实现[J].自动化与仪器仪表,2014(08):127-128+131.

[2]刘长亮. 基于眼睛与头部状态的疲劳检测系统设计[D].大连海事大学,2018.

[3]孙志. 基于OpenCV的人脸识别算法实验平台研究与实现[D].吉林大学,2014.

[4] 陶颖军.基于OpenCV的人脸识别应用[J].计算机系统应用,2012,21(03):220-223.

[5] 毛星宇,冷雪飞等.OpenCV3编程入门[M].北京电子工业出版社,2015.

[6] VIPL_Face.SeetaFace开源人脸识别引擎介绍. 微信公众号-深度学习大讲堂 2016-09-14


附录

代码

主线程部分:

头文件(OpenVideo.h)

``` 

/**

*Copyright (c) 2018 Young Fan.All Right Reserved.

*Filename:

```

*Author: Young Fan

*Date: 2018.5.29 - 7.5

*OpenCV version: 3.4.1

*IDE: Visual Studio 2017

*Description: Demo:Qt + Opecv + VS + SeetaFace(via VIPL)

*/


#pragma once


#include

#include "ui_OpenVideo.h"

#include

#include "MyThread.h" //自定义线程模板(类)

#include  //线程头文件


//SeetaFace

#include

#include

#include

#include

#include "cv.h"

#include "highgui.h"

#include "face_detection.h"

#include "face_alignment.h"


#include

#include

#include "face_identification.h"

#include "recognizer.h"

#include "face_detection.h"

#include "face_alignment.h"


#include "math_functions.h"


#include

#include

#include

#include



class OpenVideo : public QWidget

{

Q_OBJECT


public:

OpenVideo(QWidget *parent = Q_NULLPTR);


protected:

//重写绘图事件

void paintEvent(QPaintEvent *event);

//重写鼠标按下事件

void mousePressEvent(QMouseEvent *event);

//重写鼠标移动事件

void mouseMoveEvent(QMouseEvent *event);



public slots: //槽函数

void OpenVideoFile(); //打开视频文件

void OpenCamera(); //打开摄像头

void CloseCamera(); //关闭摄像头

void ScreenShot(); //拍照/截图

void REC();  //录像

void EndREC(); //结束录像

void FaceDetectionAlignment(); //人脸检测与对齐

void InputImage(); //输入图片

void FaceRecognitionProcess(); //人脸识别


void nextFrame(); //获取下一帧图片

void nextSeetaFaceProcessFrame(); //获取下一帧处理的图片

void currentDateAndTime();  //动态显示当前日期和时间

void CloseWindow(); //关闭窗口


//获取子线程信号发来的信息

//获取人脸检测与对齐信息

//void getDetectionAndAlignmentInformation(int faceNum, QImage image);

//获取人脸识别信息

void getRecognitionInformation(int gallery_face_num, int probe_face_num,QImage image,float sim);

void NoFace();


signals:


void mainSignal(QString str);

void VideoSignal(QImage image,bool flag);


private:

Ui::OpenVideoClass ui;


cv::Mat frame; //定义一个Mat变量,用于存储没一帧的图像

cv::Mat temp; //临时变量

cv::VideoCapture capture; //定义VideoCapture对象,获取摄像头

cv::VideoWriter writer;  //定义VideoWriter对象,录制

QTimer *timer;  //定时器

QTimer *timer2;  //人脸识别定时器

QString ImagePath; //定义输入图片的路径


int x = 0;

int y = 0;


bool openREC;

QPoint p; //定义的点


MyThread *myT; //自定义线程模块(类)对象

QThread *thread; //子线程


//人脸检测与对齐

int faceNum;

QImage imageDetectionAndAlignment;


//人脸识别

int GalleryFaceNum;

int ProbeFaceNum;

QImage CropFace;

float Sim;

bool CameraVideoFlag; //标志位,true代表Camera,false代表Video

};

```


cpp文件(OpenVideo.cpp)

```

#include "OpenVideo.h"

#include

#include

#include

#include

#include

#include

#include

#include

#include

//使用Qt多媒体,需要在附加依赖项里加Qt5Multimedia.lib(debug)/Qt5Multimediad.lib(release)

#include




OpenVideo::OpenVideo(QWidget *parent)

: QWidget(parent)

{

ui.setupUi(this);


//设置窗口标题

setWindowTitle("OpenVideo");


//让图片自动适应label大小

ui.labelOpenVideo->setScaledContents(true);

ui.labelGif->setScaledContents(true);


//-----【显示Gif动画】-------

//创建动画

QMovie *myMovie = new QMovie("./UiFile/2.gif");

//设置动画

ui.labelGif->setMovie(myMovie);

//启动动画

myMovie->start();


//固定主窗口大小

setFixedSize(1125, 511);


//创建定时器

timer = new QTimer(this);


//-----------【不规则窗口】-------------

//给窗口去掉边框,带上窗口的flags(标记)

setWindowFlags(Qt::FramelessWindowHint | windowFlags());

//把窗口背景设置为透明,这里不设置透明也可以,因为图片不透明

setAttribute(Qt::WA_TranslucentBackground);


//更改按钮样式表

ui.CloseButton->setFlat(true); //把按钮设置为透明背景,此时只有按下的时候才有背景。

ui.CloseButton->setStyleSheet("QPushButton{"

//"background:rgb(176,0,0);"//按钮设置为透明背景,这里设置了设置了也不显示

//但如果加上"border:2px outset green;"则可以在初始态,鼠标悬停态显示背景色,下面一样

"}"

"QPushButton:hover{"

"border:1px;"//当按钮背景是透明的,则必须加边框宽度,才能有hover效果,这样写也行"border:1px outset red;"

"background:rgb(255,96,96);"//按钮设置为透明背景,这里设置了设置了也不显示

//"border-image:url(E:/C++/Demo/OpenVideo/UiFile/closeWhite.png);"

"}"

"QPushButton:pressed{"

"background:rgb(255,77,77);"//此时只有按下的时候才有背景。

//"border-image:url(E:/C++/Demo/OpenVideo/UiFile/closeWhite.png);"

"}");


//-----------------------------------------【多线程部分】--------------------------------------------------

//自定义模块(类)对象,分配空间,不可以指定父对象,因为下面要移到子线程

myT = new MyThread;

//创建子线程,指定父对象

thread = new QThread(this);

//把自定义模块添加到子线程

myT->moveToThread(thread);

//启动子线程,但是并没有启动线程的处理函数,要用信号和槽启动线程的处理函数

thread->start();


//线程处理函数,必须通过signal-slot调用

//这里使用的是按钮的信号来触发,也可以用按钮的转到槽函数,在里面去发射信号

//,用信号去调用模块类中的处理函数

connect(ui.buttonFaceRecognition,&QPushButton::pressed,myT,&MyThread::FaceRecognitionProcess);//人脸识别部分

//一旦子模块类中的处理函数启动,就会发射UpdateProcess信号(线程代码里写的)

connect(myT,&MyThread::UpdateRecognition,this,&OpenVideo::getRecognitionInformation);//人脸识别部分

connect(myT, &MyThread::NoFace, this, &OpenVideo::NoFace);


//主线程给子线程通信

connect(this, &OpenVideo::mainSignal, myT, &MyThread::getMainSignal);

connect(this,&OpenVideo::VideoSignal,myT,&MyThread::getVideoSignal);


}

//--------------------------------------------【重写鼠标按下事件】--------------------------------------------

void OpenVideo::mousePressEvent(QMouseEvent *event)

{

if (event->button() == Qt::LeftButton) //event->button()返回的是Buttons事件

{

//求坐标差值,窗口上鼠标点击当前点的坐标是相对于屏幕的

//求差:当前点击坐标 - 窗口左上角坐标

//相对于窗口的坐标用x,y,相对于屏幕用globalPos

//frameGeometry,是带边框的,用它可以获取窗口左上角坐标(相对于屏幕的坐标)

//移动窗口时是以左上角来移动的,要想点哪就从哪移动,则要求坐标差

p = event->globalPos() - this->frameGeometry().topLeft();

}

}


void OpenVideo::mouseMoveEvent(QMouseEvent *event)

{

//这里因为是按住,故使用buttons(带‘s’的button)

//左键按住移动窗口

if (event->buttons() & Qt::LeftButton) //event->buttons()返回的是int位字段

{

//移动窗口时是以左上角来移动的,故要减去p(此时就相当于从该点开始移动)

this->move(event->globalPos() - p);

}

}


//----------------------------------------------【画背景图】--------------------------------------------------

void OpenVideo::paintEvent(QPaintEvent *event)

{

QPainter p(this);

p.drawPixmap(0, 0, width(), height(), QPixmap("./UiFile/background.png"));

}


//-----------------------------------------------【显示当前日期和时间】------------------------------------

void OpenVideo::currentDateAndTime()

{

//还有其他的显示时间日期的方法,如C语言的宏__DATE__,__TIME__、再比如QTime里也有能显示时间与日期

//用arg()也能显示,但都需要定时器

ui.labelDateTime->setText(QDateTime::currentDateTime().toString("yyyy.MM.dd hh:mm:ss.zzz A"));

}

//----------------------------------------------【关闭窗口】--------------------------------------------------

void OpenVideo::CloseWindow()

{

//写程序要养成一种习惯,要做判断

if (false == thread->isRunning()) //如果线程已停止,则按钮不在往下执行

{

return;

}

myT->setFlag(true); //跳出循环

thread->quit(); //温柔地退出

thread->wait(); //等待,回收资源

//但如果模块(类)还在工作的话(如死循环while(1),即不加这句myT->setFlag(true);),

//则线程并不会停止还是会在运行


this->close(); //关闭窗口

delete myT; //释放资源

}


//---------------------------------------------【打开视频文件】------------------------------------------------

void OpenVideo::OpenVideoFile()

{

//初始化标志位

CameraVideoFlag = false;

myT->setFlag(false); //子线程处理函数循环


//显示标签

ui.labelOpenVideo->setVisible(true);

ui.label->setVisible(true);


//创建文件对话框并获取路径

QString path = QFileDialog::getOpenFileName(this,

"open", "../", "Video(*.mp4 *.flv *.mkv *.avi)");


//读入视频

capture.open(path.toLocal8Bit().data()); //也可以path.toStdString()   


//判断定时器的激活状态

if (false == timer->isActive())

{

//启动定时器

timer->start(35);

}


//定时器连接,获取下一帧图片

connect(timer, &QTimer::timeout, this, &OpenVideo::nextFrame);

connect(timer, &QTimer::timeout,

[=]()

{

ui.label->setText(QStringLiteral("视频播放中..."));

ui.label->move(QPoint(x++, y++));

if (x >511)

{

x = 0;

y = 0;

}

}

);

}


//----------------------------------------------【打开摄像头】---------------------------------------------

void OpenVideo::OpenCamera()

{

//初始化标志位

CameraVideoFlag = true;


//显示标签

ui.labelOpenVideo->setVisible(true);


//读入摄像头

capture.open(0);


timer->start(35); //定时35ms


//定时器连接,获取下一帧图片

connect(timer, &QTimer::timeout, this, &OpenVideo::nextFrame);

}


//-----------------------------------【获取并显示下一帧图片】---------------------------------------

void OpenVideo::nextFrame()

{

capture >> frame; //读取当前帧

//图像在Qt显示前,必须将Mat型转化成QImage格式,将OpenCV中Mat的BGR格式转化成QImage的RGB格式

cvtColor(frame, temp, CV_BGR2RGB);

//转化成QImage格式

QImage image = QImage((const unsigned char *)(temp.data), temp.cols,

temp.rows, QImage::Format_RGB888);

ui.labelOpenVideo->setPixmap(QPixmap::fromImage(image));


if (!CameraVideoFlag)

{

emit VideoSignal(image, CameraVideoFlag);

}


}


//--------------------------------------【拍照/截图】------------------------------------------------

void OpenVideo::ScreenShot()

{

//将Mat型temp转为Qt的QImage型

QImage image = QImage((const unsigned char *)(temp.data), temp.cols,

temp.rows, QImage::Format::Format_RGB888);

ui.labelScreenShot->setPixmap(QPixmap::fromImage(image));

//让图片自动适应label大小

ui.labelScreenShot->setScaledContents(true);


//截图保存

const QPixmap *img = ui.labelScreenShot->pixmap();

QString path = QFileDialog::getSaveFileName(this, "save", "../", "Image(*.png)");

if (false == path.isEmpty()) //判断路径是否有效

{

bool isOk = img->save(path);

if (true == isOk)

{


//#pragma execution_character_set("utf-8")

//设置为UTF-8格式,显示中文,不然会乱码,但是影响了vs的编码格式,不能摧毁中文名称窗口

//故选择用QStringLiteral("中文")进行修饰。这样虽不能全局设置utf-8,但可以正常摧毁中文名称窗口

QMessageBox::information(this, QStringLiteral("保存完成"), QStringLiteral("截图保存完成"));

}

}

else

{

//保存文件路径出错

return;

}

}


//---------------------------------------【录制视频】-------------------------------------------

void OpenVideo::REC()

{

double rate = 12.0;//保存视频的帧率,12这个帧率很合适,播放的时候不慢不快

cv::Size videoSize(frame.cols, frame.rows);

//录制 VideoWriter writer; 已在头文件中定义

writer.open("../REC.avi", cv::VideoWriter::fourcc('M', 'J', 'P', 'G'), rate, videoSize, true);

openREC = true;

while (openREC)

{

capture >> frame;

writer.write(frame);//或writer << frame;


//加个窗口显示。不加的话,直接设置循环会卡住不动

cv::namedWindow("录制视频");

imshow("录制视频", frame);

cv::waitKey(30);

}

}


//-------------------------------------------------【结束录制】------------------------------------------

void OpenVideo::EndREC()

{

cv::destroyWindow("录制视频");

openREC = false;

writer.release();//结束录制

}


//--------------------------------------------【人脸检测与对齐】-----------------------------------------------

void OpenVideo::FaceDetectionAlignment()

{

//定时器连接,获取下一帧图片

connect(timer, &QTimer::timeout, this, &OpenVideo::nextSeetaFaceProcessFrame);

}


//-----------------------------------【获取下一帧SeetaFace处理的图片】----------------------------

void OpenVideo::nextSeetaFaceProcessFrame()

{


std::string MODEL_DIR = "./model/";


//-----------------------------------------【人脸检测与对齐】--------------------------------------

////要检测图像上的人脸,首先应该用模型文件的路径实例化seeta::FaceDetection的对象。

seeta::FaceDetection detector("./model/seeta_fd_frontal_v1.0.bin");

detector.SetMinFaceSize(40);//设置要检测的人脸的最小尺寸(默认值:20,不受限制),也能设置最大:face_detector.SetMaxFaceSize(size);

detector.SetScoreThresh(2.f);////设置检测到的人脸的得分阈值(默认值:2.0)

detector.SetImagePyramidScaleFactor(0.8f);//设置图像金字塔比例因子(0 <因子< 1,默认值:0.8)

detector.SetWindowStep(4, 4);//设置滑动窗口的步长(默认:4),face_detector.SetWindowStep(step_x, step_y);


capture >> frame;//读入摄像头


int pts_num = 5;

cv::Mat img_gray;

if (frame.channels() != 1)

//非单通道的,转为灰度图

cv::cvtColor(frame, img_gray, cv::COLOR_BGR2GRAY);

else

{

img_gray = frame;

}


seeta::ImageData image_data;

image_data.data = img_gray.data;

image_data.width = img_gray.cols;

image_data.height = img_gray.rows;

image_data.num_channels = 1;


//调用Detect()来检测人脸,它将作为seeta::FaceInfo的向量(容器)返回。

std::vector faces = detector.Detect(image_data);

int32_t face_num = static_cast(faces.size()); //获取人脸数量


seeta::FacialLandmark points[5];//定义面部的5个标记点


//首先应该实例化seeta:: faceAlignment的对象与模型文件的路径。

seeta::FaceAlignment point_detector((MODEL_DIR + "seeta_fa_v1.1.bin").c_str());


//检测5点面部标记

for (int f = 0; f < face_num; f++) {

point_detector.PointDetectLandmarks(image_data, faces[f], points);//检测出人脸标记

//将人脸用矩形框出(矩形的范围由Detect()返回的faces给出)

cv::rectangle(frame, cv::Point(faces[f].bbox.x, faces[f].bbox.y),//bbox是Rect类型的结构体

cv::Point(faces[f].bbox.x + faces[f].bbox.width - 1, faces[f].bbox.y + faces[f].bbox.height - 1), CV_RGB(255, 0, 0));


// Visualize the results(形象化结果,即把检测出的5点标记(由PointDetectLandmarks函数内部给出),用小圆环框出)

for (int i = 0; i < pts_num; i++)

{

cv::circle(frame, cv::Point(points[i].x, points[i].y), 2, CV_RGB(0, 255, 0), -CV_FILLED);

//cv::ellipse(frame, cv::Point(points[i].x, points[i].y),cv::Size(6,2),-45,0,360,cv::Scalar(255,129,0),1,8);

}

}


//图像在Qt显示前,必须转化成QImage格式,将RGBA格式转化成RGB

cvtColor(frame, temp, CV_BGR2RGB);

//转化成QImage格式

QImage image = QImage((const unsigned char *)(temp.data), temp.cols,

temp.rows, QImage::Format_RGB888);


ui.labelFaceNum->setText(QStringLiteral("检测到的人脸个数:%1").arg(face_num));

ui.labelOpenVideo->setPixmap(QPixmap::fromImage(image));


}



//----------------------------------------------【关闭摄像头】-------------------------------------------

void OpenVideo::CloseCamera()

{

capture.release();

timer->stop();


//关闭摄像头后,对标签状态进行控制,下面有好几种方法

//ui.labelOpenVideo->hide();  //控件隐藏,Widget类的方法

//ui.labelOpenVideo->setVisible(false);//控件是否可见,Widget类的方法

ui.labelOpenVideo->close();//控件关闭,Widget类的方法,可与与show()搭配使用;


//关闭录制(按钮复用)

cv::destroyWindow("录制视频");

openREC = false;


//去除“视频播放中”字样

ui.label->close();


//先停止子线程

myT->setFlag(true); //跳出循环

}


//------------------------------------------------【输入图片】-------------------------------------------

void OpenVideo::InputImage()

{

ImagePath = QFileDialog::getOpenFileName(this,

QStringLiteral("打开图片"), "../", "Image(*.png *.jpg *.bmp)");


emit mainSignal(ImagePath); //发出信号给子线程


ui.InputImageLabel->setPixmap(QPixmap(ImagePath)); //其实直接放QString类型的也可以(即直接放ImagePath)

//让图片自动适应label大小

ui.InputImageLabel->setScaledContents(true);


//动态显示当前日期和时间(定时器已在打开摄像头时激活)

connect(timer, &QTimer::timeout, this, &OpenVideo::currentDateAndTime);

}


//------------------------------------【获取子线程信号发来的信息】------------------------------------

//获取人脸识别模块的信息

void OpenVideo::getRecognitionInformation(int gallery_face_num, int probe_face_num,QImage image,float sim)

{

GalleryFaceNum = gallery_face_num;

ProbeFaceNum = probe_face_num;

CropFace = image;

Sim = sim;

//更新操作

FaceRecognitionProcess();

}


//-------------------------------------------------【人脸识别】--------------------------------------------

//描述: 从视频中找出与输入图片对应的人脸

void OpenVideo::FaceRecognitionProcess()

{


if (GalleryFaceNum != 0 && ProbeFaceNum != 0)

{

//显示相似度

ui.SimilarityLabel->setText(QStringLiteral("相似度为:%1").arg(Sim));  //组包,用arg来显示float型


if (Sim > 0.7)

{

ui.SimilarityLabel->setText(QStringLiteral("发现目标 | 相似度为:%1").arg(Sim));

//printf("\a"); //转义响铃符,在Qt中无效,改用下面的Qt的方式


//播放音频

QMediaPlayer *player = new QMediaPlayer;

//QUrl::fromLocalFile该函数接受由斜线分隔(/)的路径以及该平台的本机分隔符。

//此函数还接受具有双前导斜杠(反斜杠)(\\)的路径来指示远程文件

player->setMedia(QUrl::fromLocalFile("./mp3/Recognition.mp3"));

player->setVolume(80);

player->play();


//----------------------------------【将裁剪的脸部显示在界面标签上】------------------------------------------

ui.OutputImageLabel->setPixmap(QPixmap::fromImage(CropFace));

ui.OutputImageLabel->setScaledContents(true);

}

}


}


void OpenVideo::NoFace()

{

ui.SimilarityLabel->setText(QStringLiteral("sorry!检测不到人脸"));

}


主函数cpp文件(main.cpp)


#include "OpenVideo.h"

#include


int main(int argc, char *argv[])

{

QApplication a(argc, argv);

OpenVideo w;

w.show();

return a.exec();

}

```

子线程部分

头文件(MyThread.h)

```

#ifndef MYTHREAD_H  //防止头文件被重复包含,这是C语言方式,C++是:#pragma once

#define MYTHREAD_H


//SeetaFace

#include

#include

#include

#include

#include "cv.h"

#include "highgui.h"

#include "face_detection.h"

#include "face_alignment.h"


#include

#include

#include "face_identification.h"

#include "recognizer.h"

#include "face_detection.h"

#include "face_alignment.h"


#include "math_functions.h"


#include

#include

#include

#include


#include

#include

#include //定时器

#include //QImage头文件


class MyThread : public QObject

{

Q_OBJECT

public:

explicit MyThread(QObject *parent = nullptr);

//线程处理函数

void FaceRecognitionProcess(); //人脸识别


//获取主线程信号发来的数据

void getMainSignal(QString str);

void getVideoSignal(QImage image,bool flag);


//标志位函数

void setFlag(bool flag = true); //括号里定义了一个默认参数,不传参的时候就使用默认的


signals:

void UpdateRecognition(int GalleryFaceNum,int ProbeFaceNum,QImage image,float sim);

void NoFace();

private:

cv::Mat frame; //定义一个Mat变量,用于存储每一帧的图像

cv::Mat temp; //临时变量

cv::VideoCapture capture; //定义VideoCapture对象,获取摄像头


QImage image;


//标志位

bool isStop;

bool CameraVideoFlag;//标志位,true代表Camera,false代表Video


QString path;

};


#endif // MYTHREAD_H

```

cpp文件(MyThread.cpp)

 ```

#include "MyThread.h"


MyThread::MyThread(QObject *parent) : QObject(parent)

{

//设置初始标志位

isStop = false;

CameraVideoFlag = true;

}


void MyThread::setFlag(bool flag)

{

isStop = flag;

}


//----------------------------------------------------【人脸识别】------------------------------------------------

void MyThread::FaceRecognitionProcess()

{

using namespace seeta;


std::string MODEL_DIR = "./model/";


//用模型文件的路径实例化seeta::FaceDetection的对象

seeta::FaceDetection detector("./model/seeta_fd_frontal_v1.0.bin");

detector.SetMinFaceSize(40);//设置要检测的人脸的最小尺寸(默认值:20,不受限制),也能设置最大:face_detector.SetMaxFaceSize(size);

detector.SetScoreThresh(2.f); //设置检测到的人脸的得分阈值(默认值:2.0)

detector.SetImagePyramidScaleFactor(0.8f);//设置图像金字塔比例因子(0 <因子< 1,默认值:0.8)

detector.SetWindowStep(4, 4);//设置滑动窗口的步长(默认:4),face_detector.SetWindowStep(step_x, step_y);


//初始化的人脸对齐模型

seeta::FaceAlignment point_detector("./model/seeta_fa_v1.1.bin");


//初始化的人脸识别模型

FaceIdentification face_recognizer((MODEL_DIR + "seeta_fr_v1.0.bin").c_str());


//输入一张图片

cv::Mat gallery_img_color = cv::imread(path.toStdString(), 1);

cv::Mat gallery_img_gray;

cv::cvtColor(gallery_img_color, gallery_img_gray, CV_BGR2GRAY);


cv::Mat probe_img_color;


if (CameraVideoFlag)

{

capture.open(0);

}


while (!isStop)

{

//获取摄像头/视频的每一帧画面

if (CameraVideoFlag)

{

capture >> probe_img_color;

}

else

{

//接收QImage的格式是RGB,要转成OpenCV中Mat的BGR格式

probe_img_color = frame;

}

cv::Mat probe_img_gray;

cv::cvtColor(probe_img_color, probe_img_gray, CV_BGR2GRAY);


ImageData gallery_img_data_color(gallery_img_color.cols, gallery_img_color.rows, gallery_img_color.channels());

gallery_img_data_color.data = gallery_img_color.data;//data  uchar型的指针。Mat类分为了两个部分:矩阵头和指向矩阵数据部分(所有矩阵值)的指针,data就是指向矩阵数据的指针。


ImageData gallery_img_data_gray(gallery_img_gray.cols, gallery_img_gray.rows, gallery_img_gray.channels());

gallery_img_data_gray.data = gallery_img_gray.data;


ImageData probe_img_data_color(probe_img_color.cols, probe_img_color.rows, probe_img_color.channels());

probe_img_data_color.data = probe_img_color.data;


ImageData probe_img_data_gray(probe_img_gray.cols, probe_img_gray.rows, probe_img_gray.channels());

probe_img_data_gray.data = probe_img_gray.data;


// Detect faces

std::vector gallery_faces = detector.Detect(gallery_img_data_gray);

int32_t gallery_face_num = static_cast(gallery_faces.size());


std::vector probe_faces = detector.Detect(probe_img_data_gray);

int32_t probe_face_num = static_cast(probe_faces.size());


if (gallery_face_num != 0 && probe_face_num != 0)

{

//检测5点面部标记

seeta::FacialLandmark gallery_points[5];

point_detector.PointDetectLandmarks(gallery_img_data_gray, gallery_faces[0], gallery_points);


seeta::FacialLandmark probe_points[5];

point_detector.PointDetectLandmarks(probe_img_data_gray, probe_faces[0], probe_points);


for (int i = 0; i < 5; i++)

{

cv::circle(gallery_img_color, cv::Point(gallery_points[i].x, gallery_points[i].y), 2,

CV_RGB(0, 255, 0));

cv::circle(probe_img_color, cv::Point(probe_points[i].x, probe_points[i].y), 2,

CV_RGB(0, 255, 0));

}

cv::imwrite("gallery_point_result.jpg", gallery_img_color);

cv::imwrite("probe_point_result.jpg", probe_img_color);


//提取面部特性特征

float gallery_fea[2048];

float probe_fea[2048];

face_recognizer.ExtractFeatureWithCrop(gallery_img_data_color, gallery_points, gallery_fea);

face_recognizer.ExtractFeatureWithCrop(probe_img_data_color, probe_points, probe_fea);


//-------------------------------------------【裁剪脸部】----------------------------------------------------

//创建一个图像来存储裁剪的脸部

cv::Mat dst_img(face_recognizer.crop_height(),

face_recognizer.crop_width(),

CV_8UC(face_recognizer.crop_channels()));

ImageData dst_img_data(dst_img.cols, dst_img.rows, dst_img.channels());

dst_img_data.data = dst_img.data;


//裁剪

face_recognizer.CropFace(probe_img_data_color, probe_points, dst_img_data);

//保存裁剪的脸部

cv::imwrite("dst_img.jpg", dst_img);


//图像在Qt显示前,必须将Mat型转化成QImage格式,将RGBA格式转化成RGB

cvtColor(dst_img, temp, CV_BGR2RGB);

//转化成QImage格式

QImage CropFace = QImage((const unsigned char *)(temp.data), temp.cols,

temp.rows, QImage::Format_RGB888);


//计算两张脸部的相似度

float sim = face_recognizer.CalcSimilarity(gallery_fea, probe_fea);


//Qt多线程的信号传参只能传基本数据类型

//Mat或者库里面的int_8 / int_32(自定义的数据类型) 什么的不能传

//类型转换

int GalleryFaceNum = gallery_face_num;

int ProbeFaceNum = probe_face_num;

//发射信号

emit UpdateRecognition(GalleryFaceNum, ProbeFaceNum, CropFace, sim);

}


else

{

emit NoFace();

}

}

}


void MyThread::getMainSignal(QString str)

{

//获取输入图片的路径

path = str;

}


void MyThread::getVideoSignal(QImage image,bool flag)

{

this->image = image;

CameraVideoFlag = flag;

frame = cv::Mat(image.height(), image.width(), CV_8UC3, (void*)image.constBits(), image.bytesPerLine());

//不能放在这里转格式,会造成不能及时接收

//故把格式转换放在处理函数里了

}

```

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,030评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,198评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,995评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,973评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,869评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,766评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,967评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,599评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,886评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,901评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,728评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,504评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,967评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,128评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,445评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,018评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,224评论 2 339

推荐阅读更多精彩内容