Vision框架详细解析(十) —— 基于Vision的Body Detect和Hand Pose(一)

版本记录

版本号 时间
V1.0 2021.03.10 星期三

前言

iOS 11+macOS 10.13+ 新出了Vision框架,提供了人脸识别、物体检测、物体跟踪等技术,它是基于Core ML的。可以说是人工智能的一部分,接下来几篇我们就详细的解析一下Vision框架。感兴趣的看下面几篇文章。
1. Vision框架详细解析(一) —— 基本概览(一)
2. Vision框架详细解析(二) —— 基于Vision的人脸识别(一)
3. Vision框架详细解析(三) —— 基于Vision的人脸识别(二)
4. Vision框架详细解析(四) —— 在iOS中使用Vision和Metal进行照片堆叠(一)
5. Vision框架详细解析(五) —— 在iOS中使用Vision和Metal进行照片堆叠(二)
6. Vision框架详细解析(六) —— 基于Vision的显著性分析(一)
7. Vision框架详细解析(七) —— 基于Vision的显著性分析(二)
8. Vision框架详细解析(八) —— 基于Vision的QR扫描(一)
9. Vision框架详细解析(九) —— 基于Vision的QR扫描(二)

开始

首先看下主要内容:

Vision框架的帮助下,了解如何检测显示在相机上的手指的数量。内容来自翻译

接着看下写作环境:

Swift 5, iOS 14, Xcode 12

下面就是正文啦。

机器学习(Machine learning)无处不在,因此当Apple在2017年宣布其Core ML框架时,这并不奇怪。CoreML附带了许多工具,包括Vision(图像分析框架)。视觉分析静止图像以检测面部,读取条形码,跟踪物体等。多年来,Apple在此框架中添加了许多很酷的功能,包括2020年引入的Hand and Body Detection API。在本教程中,您将使用Vision框架中的这些Hand and Body Detection API为您带来魔力一个名为StarCount的游戏。您将用手和手指计算从天上掉下来的星星的数量。

注意:此Vision教程假定您具有SwiftUIUIKitCombine的工作知识。有关SwiftUI的更多信息,请参见SwiftUI: Getting Started

StarCount需要具有前置摄像头的设备才能运行,因此您不能随身携带模拟器。

最后,如果您可以将设备支撑在某个地方,那将很有帮助,您将需要双手来匹配这些高数字!

在Xcode中打开starter项目。

构建并运行。 点击左上角的Rain,欣赏场景。 不要忘了对那些星星的祝福!

星星下雨的魔力在StarAnimatorView.swift中。它使用UIKit Dynamics API。如果您有兴趣,请随时查看。

该应用程序看起来不错,但可以想象一下,如果在后台显示您的实时视频,效果会更好!如果手机看不到手指,Vision无法计数手指。


Getting Ready for Detection

Vision使用静止图像进行检测。信不信由你,您在相机取景器中看到的实际上是一堆静止图像。在检测到任何东西之前,您需要将摄影机会话集成到游戏中。

1. Creating the Camera Session

要在应用程序中显示摄像机预览,请使用CALayer的子类AVCaptureVideoPreviewLayer。您可以将此预览层与capture session结合使用。

由于CALayerUIKit的一部分,因此您需要创建一个包装器才能在SwiftUI中使用它。幸运的是,Apple提供了一种使用UIViewRepresentableUIViewControllerRepresentable的简便方法。

实际上,StarAnimator是一个UIViewRepresentable,因此您可以在SwiftUI中使用StarAnimatorViewUIView的子类)。

注意:您可以在以下精彩的视频课程中了解有关将UIKitSwiftUI集成的更多信息:Integrating UIKit & SwiftUI

您将在以下部分中创建三个文件:CameraPreview.swiftCameraViewController.swiftCameraView.swift。 从CameraPreview.swift开始。

CameraPreview

StarCount组中创建一个名为CameraPreview.swift的新文件,然后添加:

// 1
import UIKit
import AVFoundation

final class CameraPreview: UIView {
  // 2
  override class var layerClass: AnyClass {
    AVCaptureVideoPreviewLayer.self
  }
  
  // 3
  var previewLayer: AVCaptureVideoPreviewLayer {
    layer as! AVCaptureVideoPreviewLayer 
  }
}

在这里,您:

  • 1) 由于CameraPreviewUIView的子类,因此请导入UIKit。 您还可以导入AVFoundation,因为AVCaptureVideoPreviewLayer是此模块的一部分。
  • 2) 接下来,您覆盖静态layerClass。 这使得该视图的根层类型为AVCaptureVideoPreviewLayer
  • 3) 然后,创建一个称为PreviewLayer的计算属性,并将此视图的根层强制转换为您在第二步中定义的类型。 现在,当您以后需要使用它时,可以使用此属性直接访问该层。

接下来,您将创建一个视图控制器来管理CameraPreview

CameraViewController

AVFoundation的相机捕获代码旨在与UIKit配合使用,因此要使其在您的SwiftUI应用中正常工作,您需要制作一个视图控制器并将其包装在UIViewControllerRepresentable中。

StarCount组中创建CameraViewController.swift并添加:

import UIKit

final class CameraViewController: UIViewController {
  // 1
  override func loadView() {
    view = CameraPreview()
  }
  
  // 2
  private var cameraView: CameraPreview { view as! CameraPreview }
}

在这里你:

  • 1) 重写loadView以使视图控制器将CameraPreview用作其根视图。
  • 2) 创建一个名为cameraPreview的计算属性,以CameraPreview的身份访问根视图。 您可以安全地在此处强制赋值,因为您最近在第一步中分配了CameraPreview实例给view

现在,您将制作一个SwiftUI视图以包装新的视图控制器,以便可以在StarCount中使用它。

CameraView

StarCount组中创建CameraView.swift并添加:

import SwiftUI

// 1
struct CameraView: UIViewControllerRepresentable {
  // 2
  func makeUIViewController(context: Context) -> CameraViewController {
    let cvc = CameraViewController()
    return cvc
  }

  // 3
  func updateUIViewController(
    _ uiViewController: CameraViewController, 
    context: Context
  ) {
  }
}

这就是上面的代码中发生的事情:

  • 1) 您创建一个名为CameraView的结构体,该结构体符合UIViewControllerRepresentable。 这是用于制作包装UIKit视图控制器的SwiftUI View类型的协议。
  • 2) 您实现第一个协议方法,makeUIViewController。 在这里,您将初始化CameraViewController的实例,并执行一次仅一次的设置。
  • 3) updateUIViewController(_:context :)是该协议的另一个必需方法,您可以在其中基于SwiftUI数据或层次结构的更改对视图控制器进行任何更新。 对于此应用,您无需在此处做任何事情。

完成所有这些工作之后,该在ContentView中使用CameraView了。

打开ContentView.swift。 在bodyZStack的开头插入CameraView

CameraView()
  .edgesIgnoringSafeArea(.all)

那是一个很长的部分。 构建并运行以查看您的相机预览。

所有的工作都没有改变! 为什么? 在相机预览工作之前,还需要添加另一个难题,即AVCaptureSession。 接下来,您将添加该内容。

2. Connecting to the Camera Session

您将在此处进行的更改似乎很长,但是请不要害怕。 它们大多是样板代码。

打开CameraViewController.swift。 在import UIKit之后添加以下内容:

import AVFoundation 

然后,在类内添加AVCaptureSession类型的实例属性:

private var cameraFeedSession: AVCaptureSession?

最好在此视图控制器出现在屏幕上时运行capture session,并在视图不再可见时停止session,因此添加以下内容:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  
  do {
    // 1
    if cameraFeedSession == nil {
      // 2
      try setupAVSession()
      // 3
      cameraView.previewLayer.session = cameraFeedSession
      cameraView.previewLayer.videoGravity = .resizeAspectFill
    }
    
    // 4
    cameraFeedSession?.startRunning()
  } catch {
    print(error.localizedDescription)
  }
}

// 5
override func viewWillDisappear(_ animated: Bool) {
  cameraFeedSession?.stopRunning()
  super.viewWillDisappear(animated)
}

func setupAVSession() throws {
}

以下是代码细分:

  • 1) 在viewDidAppear(_ :)中,检查是否已经初始化了cameraFeedSession
  • 2) 您调用setupAVSession(),该函数目前为空,但很快就会实现。
  • 3) 然后,将会话设置为cameraViewPreviewLayer的会话,并设置视频的调整大小模式。
  • 4) 接下来,您开始运行会话。 这使camera feed可见。
  • 5) 在viewWillDisappear(_ :)中,关闭camera feed以延长电池寿命。

现在,您将添加缺少的代码以准备相机。

Preparing the Camera

为调度队列添加一个新属性,Vision将在该属性上处理摄像机采样:

private let videoDataOutputQueue = DispatchQueue(
  label: "CameraFeedOutput", 
  qos: .userInteractive
)

添加扩展以使视图控制器符合AVCaptureVideoDataOutputSampleBufferDelegate

extension 
CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
}

有了这两件事之后,您现在可以替换空的setupAVSession()了:

func setupAVSession() throws {
  // 1
  guard let videoDevice = AVCaptureDevice.default(
    .builtInWideAngleCamera, 
    for: .video, 
    position: .front) 
  else {
    throw AppError.captureSessionSetup(
      reason: "Could not find a front facing camera."
    )
  }

  // 2
  guard 
    let deviceInput = try? AVCaptureDeviceInput(device: videoDevice)
  else {
    throw AppError.captureSessionSetup(
      reason: "Could not create video device input."
    )
  }

  // 3
  let session = AVCaptureSession()
  session.beginConfiguration()
  session.sessionPreset = AVCaptureSession.Preset.high

  // 4
  guard session.canAddInput(deviceInput) else {
    throw AppError.captureSessionSetup(
      reason: "Could not add video device input to the session"
    )
  }
  session.addInput(deviceInput)

  // 5
  let dataOutput = AVCaptureVideoDataOutput()
  if session.canAddOutput(dataOutput) {
    session.addOutput(dataOutput)
    dataOutput.alwaysDiscardsLateVideoFrames = true
    dataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
  } else {
    throw AppError.captureSessionSetup(
      reason: "Could not add video data output to the session"
    )
  }
  
  // 6
  session.commitConfiguration()
  cameraFeedSession = session
}

在您上面的代码中:

  • 1) 检查设备是否带有前置摄像头。 如果不是,则抛出错误。
  • 2) 接下来,检查是否可以使用相机创建捕捉设备输入(capture device input)
  • 3) 创建一个capture session并开始使用高质量预设(high quality preset)进行配置。
  • 4) 然后检查会话是否可以capture device输入。 如果是,请将您在第二步中创建的输入添加到会话中。 您需要输入和输出才能使会话正常工作。
  • 5) 接下来,创建数据输出并将其添加到会话中。 数据输出将从相机源中获取图像样本,并将它们提供给您在先前设置的已定义调度队列中的委托中。
  • 6) 最后,完成配置会话并将其分配给您之前创建的属性。

构建并运行。 现在,您可以看到自己在雨星的背后。

注意:您需要用户权限才能访问设备上的相机。 首次启动摄像头会话时,iOS会提示用户授予对摄像头的访问权限。 您必须向用户说明您希望获得摄像头许可的原因。

Info.plist中的键值对存储原因。 在入门项目中已经存在。

有了这一点之后,就该转移到Vision了。


Detecting Hands

要在Vision中使用任何算法,通常需要遵循以下三个步骤:

  • 1) Request:您通过定义请求特征来请求框架为您检测到某些东西。 您使用VNRequest的适当子类。
  • 2) Handler:接下来,您要求框架在请求完成执行或处理请求之后执行一种方法。
  • 3) Observation:最后,您可以获得潜在的结果或观察结果。 这些观察是基于您的请求的VNObservation的实例。

您将首先处理该请求。

1. Request

用于检测手的请求的类型为VNDetectHumanHandPoseRequest

仍在CameraViewController.swift中,在import AVFoundation之后添加以下内容以访问Vision框架:

import Vision

然后,在类定义内,创建以下实例属性:

private let handPoseRequest: VNDetectHumanHandPoseRequest = {
  // 1
  let request = VNDetectHumanHandPoseRequest()
  
  // 2
  request.maximumHandCount = 2
  return request
}()

在这里你:

  • 1) 创建一个检测人手的请求。
  • 2) 将要检测的最大手数设置为两个。 Vision框架功能强大。 它可以检测到图像中的许多手。 由于最多十颗星落在任何一滴中,因此两只手和十根手指就足够了。

现在,是时候设置处理handlerobservation了。

2. Handler and Observation

您可以使用AVCaptureVideoDataOutputSampleBufferDelegate从采集流中获取样本并开始检测过程。

在您之前创建的CameraViewController扩展中实现此方法:

func captureOutput(
  _ output: AVCaptureOutput, 
  didOutput sampleBuffer: CMSampleBuffer, 
  from connection: AVCaptureConnection
) {
  // 1
  let handler = VNImageRequestHandler(
    cmSampleBuffer: sampleBuffer, 
    orientation: .up, 
    options: [:]
  )

  do {
    // 2
    try handler.perform([handPoseRequest])

    // 3
    guard 
      let results = handPoseRequest.results?.prefix(2), 
      !results.isEmpty 
    else {
      return
    }

    print(results)
  } catch {
    // 4
    cameraFeedSession?.stopRunning()
  }
}

以下是代码细分:

  • 1) 只要有采样,就会调用captureOutput(_:didOutput:from :)。在此方法中,您将创建一个handler,这是使用Vision所需的第二步。您将获得的样本缓冲区(sample buffer)作为输入参数传递,以对单个图像执行请求。

  • 2) 然后,您执行请求。如果有任何错误,此方法将引发错误,因此它位于do-catch块中。执行请求是同步操作。还记得您提供给代理回调的调度队列吗?这样可以确保您不会阻塞主队列。Vision完成了该后台队列上的检测过程。

  • 3) 您可以使用请求的results获得检测结果或观察结果。在这里,您可以获得前两项,并确保结果数组不为空。当您在创建请求时只要求两只手时,这是一种额外的预防措施,可确保您得到的结果项不超过两个。接下来,将结果打印到控制台。

  • 4) 如果请求失败,则意味着发生了一些不好的事情。在生产环境中,您可以更好地处理此错误。目前,您可以停止摄像头会话。

构建并运行。将您的手放在相机前面,然后查看Xcode控制台。

在控制台中,您将看到可见的VNHumanHandPoseObservation类型的观察对象。 接下来,您将从这些观察结果中提取手指数据。 但是首先,您需要阅读一下解剖学!

3. Anatomy to the Rescue!

Vision框架会详细检测手。 查看以下插图:

此图像上的每个圆圈都是一个LandmarkVision可以检测到每只手的21landmarks:每个手指四个,拇指四个和手腕一个。

这些手指中的每个手指都在一个Joints Group中,由VNHumanHandPoseObservation.JointsGroupName中的API将其描述为:

  • .thumb
  • .indexFinger
  • .middleFinger
  • .ringFinger
  • .littleFinger

在每个关节组中,每个关节都有一个名称:

  • TIP:指尖。
  • DIP:指间远端关节或指尖后的第一个关节。
  • PIP:指间近关节或中间关节。
  • MIP:掌指关节位于手指底部,与手掌相连。

拇指有点不同。 它有一个TIP,但其他关节具有不同的名称:

  • TIP:拇指尖。
  • IP:指间关节至拇指尖后的第一个关节。
  • MP:掌指关节位于拇指底部,与手掌相连。
  • CMC:腕掌关节在手腕附近。

许多开发人员认为自己的职业不需要数学。 谁会想到解剖学也是前提?

了解了解剖结构,是时候检测指尖了。

4. Detecting Fingertips

为简单起见,您将检测到指尖并在顶部绘制一个覆盖图。

CameraViewController.swift中,将以下内容添加到captureOutput(_:didOutput:from :)的顶部:

var fingerTips: [CGPoint] = []

这将存储检测到的指尖。 现在,将您在上一步中添加的print(results)替换为:

var recognizedPoints: [VNRecognizedPoint] = []

try results.forEach { observation in
  // 1
  let fingers = try observation.recognizedPoints(.all)

  // 2
  if let thumbTipPoint = fingers[.thumbTip] {
    recognizedPoints.append(thumbTipPoint)
  }
  if let indexTipPoint = fingers[.indexTip] {
    recognizedPoints.append(indexTipPoint)
  }
  if let middleTipPoint = fingers[.middleTip] {
    recognizedPoints.append(middleTipPoint)
  }
  if let ringTipPoint = fingers[.ringTip] {
    recognizedPoints.append(ringTipPoint)
  }
  if let littleTipPoint = fingers[.littleTip] {
    recognizedPoints.append(littleTipPoint)
  }
}

// 3
fingerTips = recognizedPoints.filter {
  // Ignore low confidence points.
  $0.confidence > 0.9
}
.map {
  // 4
  CGPoint(x: $0.location.x, y: 1 - $0.location.y)
}

在这里你:

  • 1) 获取所有手指的分数。
  • 2) 寻找尖点。
  • 3) 每个VNRecognizedPoint都有一个置信度confidence。 您只需要具有高置信度的观察值。
  • 4) Vision算法使用左下原点的坐标系,并返回相对于输入图像像素尺寸的归一化值。 AVFoundation坐标具有左上角的原点,因此您可以转换y坐标。

您需要使用这些指尖进行操作,因此将以下内容添加到CameraViewController中:

// 1
var pointsProcessorHandler: (([CGPoint]) -> Void)?

func processPoints(_ fingerTips: [CGPoint]) {
  // 2
  let convertedPoints = fingerTips.map {
    cameraView.previewLayer.layerPointConverted(fromCaptureDevicePoint: $0)
  }

  // 3
  pointsProcessorHandler?(convertedPoints)
}

在这里你:

  • 1) 为闭包添加一个属性,以在框架检测到点时运行。
  • 2) 从AVFoundation相对坐标转换为UIKit坐标,以便可以在屏幕上绘制它们。 您使用layerPointConverted,这是AVCaptureVideoPreviewLayer中的一种方法。
  • 3) 您可以使用转换后的点来调用闭包。

captureOutput(_:didOutput:from :)中,在声明fingerTips属性之后,添加:

defer {
  DispatchQueue.main.sync {
    self.processPoints(fingerTips)
  }
}

方法完成后,这会将您的指尖发送到主队列中进行处理。

是时候向用户展示这些指尖了!

5. Displaying Fingertips

pointsProcessorHandler将在屏幕上获取检测到的指纹。 您必须将闭包从SwiftUI传递到此视图控制器。

返回CameraView.swift并添加一个新属性:

var pointsProcessorHandler: (([CGPoint]) -> Void)?

这为您提供了在视图中存储闭包的位置。

然后通过在return语句之前添加以下行来更新makeUIViewController(context :)

cvc.pointsProcessorHandler = pointsProcessorHandler

这会将闭包传递给视图控制器。

打开ContentView.swift并将以下属性添加到视图定义:

@State private var overlayPoints: [CGPoint] = []

此状态变量将保存在CameraView中获取的点。 用以下内容替换CameraView()行:

CameraView {
  overlayPoints = $0
}

该闭包是您之前添加的pointsProcessorHandler,当您检测到点时会调用该闭包。 在闭包中,将点分配给overlayPoints

最后,在edgesIgnoringSafeArea(.all)修饰符之前添加此修饰符:

.overlay(
  FingersOverlay(with: overlayPoints)
    .foregroundColor(.orange)
)

您正在将叠加层修改器添加到CameraView。 在该修饰符内,使用检测到的点初始化FingersOverlay并将颜色设置为橙色。FingersOverlay.swift在启动项目中。 它的唯一工作是在屏幕上绘制点。

构建并运行。 检查手指上的橙色点。 移动您的手,并注意点跟随您的手指。

注意:如果需要,可以随时在.overlay修改器中更改颜色。

终于可以添加游戏逻辑了。


Adding Game Logic

游戏的逻辑很长,但是非常简单。

打开GameLogicController.swift并将类实现替换为:

// 1
private var goalCount = 0

// 2
@Published var makeItRain = false

// 3
@Published private(set) var successBadge: Int?

// 4
private var shouldEvaluateResult = true

// 5
func start() {
  makeItRain = true
}

// 6
func didRainStars(count: Int) {
  goalCount = count
}

// 7
func checkStarsCount(_ count: Int) {
  if !shouldEvaluateResult {
    return
  }
  if count == goalCount {
    shouldEvaluateResult = false
    successBadge = count

    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
      self.successBadge = nil
      self.makeItRain = true
      self.shouldEvaluateResult = true
    }
  }
}

这是一个细分:

  • 1) 此属性存储掉落的星星数。玩家必须通过显示适当的手指数来猜测该值。
  • 2) 每当将发布的属性设置为true时,StarAnimator就会开始下雨。
  • 3) 如果玩家正确猜测掉落的星星数量,则可以为此分配目标数。该值出现在屏幕上,指示成功。
  • 4) 此属性可防止过多的评估。如果玩家正确猜出了该值,则此属性将使评估停止。
  • 5) 游戏就是这样开始的。当出现开始屏幕时,您可以调用此命令。
  • 6) 当StarAnimator落下特定数量的星星时,它将调用此方法以将目标计数保存在游戏引擎中。
  • 7) 这就是魔术发生的地方。只要有新的点,就可以调用此方法。首先检查是否可以评估结果。如果猜测值正确,它将停止评估,设置成功标志值,并在三秒钟后将引擎状态重置为初始值。

打开ContentView.swift连接GameLogicController

将对StarAnimator的调用(包括其结尾的闭包)替换为:

StarAnimator(makeItRain: $gameLogicController.makeItRain) {
  gameLogicController.didRainStars(count: $0)
}

此代码向游戏引擎报告下雨天的数量。

接下来,您将告知玩家正确的答案。

1. Adding a Success Badge

successBadge添加计算的属性,如下所示:

@ViewBuilder
private var successBadge: some View {
  if let number = gameLogicController.successBadge {
    Image(systemName: "\(number).circle.fill")
      .resizable()
      .imageScale(.large)
      .foregroundColor(.white)
      .frame(width: 200, height: 200)
      .shadow(radius: 5)
  } else {
    EmptyView()
  }
}

如果游戏逻辑控制器的successBadge具有值,则可以使用SFSymbols中可用的系统映像来创建映像。 否则,您将返回EmptyView,这意味着什么都没有绘制。

将这两个修饰符添加到根ZStack中:

.onAppear {
  // 1
  gameLogicController.start()
}
.overlay(
  // 2
  successBadge
    .animation(.default)
)

这是您添加的内容:

  • 1) 当游戏的开始页面出现时,您开始游戏。
  • 2) 您将success badge置于一切之上。 接下来是successBadge实现。

接下来,删除Rain的叠加层,因为现在它会自动下雨。

2. Final Step

要使游戏正常运行,您需要将检测到的点数传递给游戏引擎。 更新在ContentView中初始化CameraView时传递的闭包:

CameraView {
  overlayPoints = $0
  gameLogicController.checkStarsCount($0.count)
}

构建并运行。 玩的开心。


More Use Cases

您几乎刚刚涉及到Vision中的Hand and Body Detection APIs。 该框架可以检测到多个body landmarks,如下所示:

以下是您可以使用这些API进行操作的一些示例:

  • 使用Vision框架在您的应用中安装UI控件。 例如,某些相机应用程序包含一些功能,可让您显示手势来拍照。
  • 构建一个有趣的表情符号应用程序,使用户可以用手显示表情符号。
  • 构建一个锻炼分析应用程序,用户可以在其中找到他或她是否在执行特定的操作。
  • 构建一个音乐应用程序,教用户弹吉他或夏威夷四弦琴。

Vision和这些特定的API有很多很棒的资源。 要更深入地探讨此主题,请尝试:

后记

本篇主要讲述了基于VisionBody DetectHand Pose,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容