0#01 解析mobile_ssd样例代码

1.预备知识
2.样例程序介绍
3.运行代码
3.1.能在笔记本上成功运行 mobilenet_ssd 样例代码。
3.2.理解 Tengine 的工作方式。
3.3.能在 RK3399 上运行该程序。
3.4.能在 RK3399 上运行该程序并使用 GPU 加速

1.预备知识

       CLion:一款优秀的C语言开发工具。主要是有免费的社区版本,并且能支持cmake文件的编译。最大的福利就是支持Ubuntu安装。
       环境安装:0#00 Tengine 的安装

2.样例程序介绍

       样例程序位于tengine目录下," tengine安装路径/examples/mobilenet_ssd/"
       我们使用 CLion 打开该项目。
1.选中mobilenet_ssd文件夹
2.右键->open with(打开方式)->Other Applications->View All Applications->CLion(使用CLion打开该文件夹。)
       打开该文件的目的:
1.能在笔记本上成功运行 mobilenet_ssd 样例代码。
2.理解 Tengine 的工作方式。
3.能在 RK3399 上运行该程序。
4.能在 RK3399 上运行该程序并使用 GPU 加速
注:当前阶段并不去理解 SSD 的工作方式,因为官方已经为我们提供训练好的 caffe 模型( prototxt + caffemodel )


从百度云中下载 caffe 模型 Tengine model zoo (密码: 57vb)
找到:

  • MobileNetSSD_deploy.caffemodel
  • MobileNetSSD_deploy.prototxt
    将其存放在 tengine路径下/models/

3.运行代码

TODO:1.能在笔记本上成功运行 mobilenet_ssd 样例代码。

       如果直接使用 CLion 对该程序进行编译,会产生报错信息,类似缺少(找不到)头文件。

/usr/bin/cmake --build /home/lee/Documents/tengine/examples/mobilenet_ssd/cmake-build-debug --target MSSD -- -j4
......(省略)
/common.cpp:4:27: fatal error: tengine_c_api.h: 没有那个文件或目录
compilation terminated.
CMakeFiles/MSSD.dir/build.make:86: recipe for target 'CMakeFiles/MSSD.dir/home/lee/Documents/tengine/examples/common/common.cpp.o' failed
make[3]: *** [CMakeFiles/MSSD.dir/home/lee/Documents/tengine/examples/common/common.cpp.o] Error 1
make[3]: *** Waiting for unfinished jobs....
/home/lee/Documents/tengine/examples/mobilenet_ssd/mssd.cpp:32:27: fatal error: tengine_c_api.h: 没有那个文件或目录
compilation terminated.
CMakeFiles/MSSD.dir/build.make:62: recipe for target 'CMakeFiles/MSSD.dir/mssd.cpp.o' failed
......(省略)

原因在官方给的 gpu_cpu_mssd.md 中提到:

cd example/mobilenet_ssd
cmake -DTENGINE_DIR=/home/firefly/tengine .
make 

所以问题出在 -DTENGINE_DIR 指定 Tengine 的安装路径。
解决办法:
在 CMAKE 文件中加入该行。

cmake_minimum_required (VERSION 2.8)
project(MSSD)
add_definitions(-std=c++11)
# TODO:设置 TENGINE_DIR 的路径
set( TENGINE_DIR /home/lee/Documents/tengine )
set( INSTALL_DIR ${TENGINE_DIR}/install/)
set( TENGINE_LIBS tengine)

       那为什么不使用官方的方法,在CMAKE中加入一个选项呢?
这是我的一些尝试得到的经验:
1.不方便,每次 CMAKE 加入该行,有时我们使用的 tengine 路径和官方不一样,CMAKE 不会报错,修改的时候,要删除一些 CAMKE 产生的内容,才能重新 CMAKE。
2.无法利用 CLion 提供的优势,CLion 每次 CMAKE 会产生一个类似 build 的文件夹,方便管理,不需要再手动删除了。
3.直接编写,能为后续报错信息提供一些思路,有时候使用 ”-DTENGINE_DIR“ 进行指定路径 启动的减号是有中/英文字符区别的,会产生莫名奇妙的错误。
       再次编译cmake文件就会没有报错信息了,注意:使用的 Tengine 路径需要修改为适合自己环境特征的。

clion.png

       运行该程序:

image.png

       所以我们已经完全能够 运行 该 样例 了。
任务1:完成。

TODO:2.理解 Tengine 的工作方式。

       任务1:只是单纯的运行程序,测试结果,和 example 差不多,类似于 我们安装的 Tengine 是否能够正常工作。
       任务2:已知 MobileNet_SSD 能正常工作的前提下,进行代码的分析。
       如果对于 Tengine 一无所知的话,可以利用 "MobileNet_SSD"进行理解,并且可以充分的利用 CLion 这个工具,进行函数查找,OAID 为 Tengine 编写的 "tengine_c_api.h" 文件提供了丰富的注释信息,我们可以将其改为中文,以便确认我们使用了哪些函数。
       我的经历总结如下:

TODO:1. 调用 init_tengine() 对 tengine进行初始化
TODO:2. 调用 create_graph 将 训练好的模型 导入到 图(网络)中
TODO:3.1通过 get_graph_input_tensor 获取输入的 tensor
TODO:3.2用 set_tensor_shape 设置输入Tensor的shape
TODO: 4. 调用 prerun_graph 函数预启动图(类似 malloc,申请资源)
TODO: 5.1.向 input_data 写入输入的数据,
TODO: 5.2.并调用 set_tensor_buffer 把数据转移到输入Tensor上
TODO: 6. 调用 run_graph 运行图(做一次前向传播)
TODO: 7.1 向 input_data 写入输入的数据
TODO: 8. 调用 run_graph 运行图(做一次前向传播)
TODO: 9.1.调用 get_graph_output_tensor 获取输出Tensor
TODO: 9.2.并用 get_tensor_shape 取得 tensor 的shape
TODO: 9.3 利用get_tensor_buffer 取得输出 tensor 的内容

最后在退出程序前依次释放各个申请的动态空间

TODO: 9.4 释放 out_tensor 所占空间
TODO: 3.3 释放 input_tensor 所占空间
TODO: 2.2 调用 destroy_graph() 来释放资源
TODO: 1.1 释放 tengine

       很明显,我们使用 tengine 只是用来进行前向计算(forward),并不需要后向反馈(backward)。
       所以程序很有模板的感觉,这也为使用 tengine 降低了难度。
       一些总结:tengine 和我常常使用的 PyTorch 进行前向推导并不太相同,

假设 模型为 Net,输入为 input,输出为 output。
PyTorch:output = Net(input)
tengine:
1. 导入模型:
load_model(Net,,"caffe", proto文件路径, model_file文件路径)
2.给模型输入:
get_graph_input_tensor()
3.获取模型输出
get_graph_output_tensor()
有点面向对象的操作。

接下来开始阅读示例代码(main函数):

1.定义默认参数变量名称

    // root_path 为 Tengine 的文件路径
    const std::string root_path = get_root_path();
    // proto_file 为我们设置的 prototxt 文件路径
    std::string proto_file;
    // model_file 为我们设置的 model 文件路径
    std::string model_file;
    // image_file 为我们设置的 image 文件路径
    std::string image_file;
    // 设置保存的 生成图片 路径名称
    std::string save_name="save.jpg";
    // device 使用设备默认为空(用于graph)
    const char * device=nullptr;

2.处理命令行的输入处理

    int res;
    // 与命令行输入相关, 指定可输入参数(-p -m -i -hd)
/*
*   -p      prototxt 文件路径       默认:"models/MobileNetSSD_deploy.prototxt"
*   -m      model 文件路径          默认:"models/MobileNetSSD_deploy.caffemodel"
*   -i      image 文件路径          默认:"tests/images/ssd_dog.jpg"
*   -h      help 信息
*   -d      device 设置            默认:为空
*/

    while( ( res=getopt(argc,argv,"p:m:i:hd:"))!= -1)
    {
        switch(res)
        {
            case 'p':
                proto_file=optarg;
                break;
            case 'm':
                model_file=optarg;
                break;
            case 'i':
                image_file=optarg;
                break;
            case 'd':
                device=optarg;
                break;
            case 'h':
                std::cout << "[Usage]: " << argv[0] << " [-h]\n"
                          << "   [-p proto_file] [-m model_file] [-i image_file]\n";
                return 0;
            default:
                break;
        }
    }

3.设置 命令行参数默认信息

    // prototxt 默认路径
    if(proto_file.empty())
    {
        proto_file = root_path + DEF_PROTO;
        std::cout << "proto file not specified,using " << proto_file << " by default\n";
    }
    // caffemodel 默认路径
    if(model_file.empty())
    {
        model_file = root_path + DEF_MODEL;
        std::cout << "model file not specified,using " << model_file << " by default\n";
    }
    // 使用图片的  默认路径
    if(image_file.empty())
    {
        image_file = root_path + DEF_IMAGE;
        std::cout << "image file not specified,using " << image_file << " by default\n";
    }

开始使用 OAID 提供的 tengine_c_api.h。(建议配合 ctrl+鼠标点开对应的函数名称,进行查看作用)
4.完成TODO1,2

    // TODO:1. 调用 init_tengine() 对 tengine进行初始化
    if(init_tengine() < 0)
    {
        std::cout << " init tengine failed\n";
        return 1;
    }
    // 检查库文件是否高于0.9
    if(request_tengine_version("0.9") != 1)
    {
        std::cout << " request tengine version failed\n";
        return 1;
    }
    // 确认 模型文件存在
    if(!check_file_exist(proto_file) or (!check_file_exist(model_file) or !check_file_exist(image_file)))
    {
        return 1;
    }
    // TODO:2. 调用 create_graph 将 训练好的模型 导入到 图(网络)中
    graph_t graph = create_graph(nullptr, "caffe", proto_file.c_str(), model_file.c_str());

    // 判断是否创建 graph 成功
    if(graph == nullptr)
    {
        std::cout << "Create graph failed\n";
        std::cout << " ,errno: " << get_tengine_errno() << "\n";
        return 1;
    }
    // 设置 运行 的设备
    if(device != nullptr)
    {
        set_graph_device(graph, device);
    }

5.设置我们处理图片的信息
       在 SSD 论文中,对于图形的输入需要先进行 reshape。

    // 输入图片信息
    // img 的高(height)
    int img_h = 300;
    // img 的宽(width)
    int img_w = 300;
    // img 的大小(size)
    int img_size = img_h * img_w * 3;
    // 为输入图片申请空间,有mallloc,所以需要释放(free)
    float *input_data = (float *)malloc(sizeof(float) * img_size);

    int node_idx=0;
    int tensor_idx=0;

6.对于模型输入的预处理,为了得到输入的入口

    // TODO:1. 调用 init_tengine() 对 tengine进行初始化
    if(init_tengine() < 0)
    {
        std::cout << " init tengine failed\n";
        return 1;
    }
    // 检查库文件是否高于0.9
    if(request_tengine_version("0.9") != 1)
    {
        std::cout << " request tengine version failed\n";
        return 1;
    }
    // 确认 模型文件存在
    if(!check_file_exist(proto_file) or (!check_file_exist(model_file) or !check_file_exist(image_file)))
    {
        return 1;
    }
    // TODO:2. 调用 create_graph 将 训练好的模型 导入到 图(网络)中
    graph_t graph = create_graph(nullptr, "caffe", proto_file.c_str(), model_file.c_str());

    // 判断是否创建 graph 成功
    if(graph == nullptr)
    {
        std::cout << "Create graph failed\n";
        std::cout << " ,errno: " << get_tengine_errno() << "\n";
        return 1;
    }
    // 设置 运行 的设备
    if(device != nullptr)
    {
        set_graph_device(graph, device);
    }

7.多次重复,获取平均值(设置重复属性)

    // 设置重复次数,求平均值
    int repeat_count = 1;
    // getenv 获取环境变量
    const char* repeat = std::getenv("REPEAT_COUNT");

    if(repeat)
        /* strtoul: (str to ul)将字符类型转为unsigned long类型,
         * 10:十进制   NULL:结束符
         */
        repeat_count = std::strtoul(repeat, NULL, 10);

8.实际输入属性

    //TODO: 5.1.向 input_data 写入输入的数据,
    // 将 图片 的路径名转化为 图片内容的矩阵,并赋值给 input_data
    get_input_data_ssd(image_file, input_data, img_h, img_w);
    //TODO: 5.2.并调用 set_tensor_buffer 把数据转移到输入Tensor上
    set_tensor_buffer(input_tensor, input_data, img_size * 4);

9.实际运行

    //TODO: 6. 调用 run_graph 运行图(做一次前向传播)
    ret = run_graph(graph, 1);
    if(ret != 0)
    {
        std::cout << "Run graph failed, errno: " << get_tengine_errno() << "\n";
        return 1;
    }

10.利用重复获取平均运行时间

    struct timeval t0, t1;
    float total_time = 0.f;
    for(int i = 0; i < repeat_count; i++)
    {
        //TODO: 7.1 向 input_data 写入输入的数据,
        get_input_data_ssd(image_file, input_data, img_h, img_w);
        // 获取当前值,并赋值给 t0(开始值)
        gettimeofday(&t0, NULL);
        //TODO: 8. 调用 run_graph 运行图(做一次前向传播)
        run_graph(graph, 1);
        gettimeofday(&t1, NULL);
        float mytime = ( float )((t1.tv_sec * 1000000 + t1.tv_usec) - (t0.tv_sec * 1000000 + t0.tv_usec)) / 1000;
        total_time += mytime;
    }
    std::cout << "--------------------------------------\n";
    std::cout << "repeat " << repeat_count << " times, avg time per run is " << total_time / repeat_count << " ms\n";

11.获取输出信息

    //TODO: 9.1.调用 get_graph_output_tensor 获取输出Tensor
    tensor_t out_tensor = get_graph_output_tensor(graph, 0, 0);    //"detection_out");
    /*
 * out_dim[4]中元素的含义
 * [0]:批次:1张图
 * [1]:检测到目标个数:3个目标
 * [2]:outdata 的 Box 6 个信息:
 *              0. 属于的类别(下标)
 *              1. 属于该类别的score
 *              2. 左上角点(x)相对于宽的百分比
 *              3. 左上角点(y)相对于高的百分比
 *              4. 右上角点(x)相对于宽的百分比
 *              5. 右上角点(y)相对于高的百分比
 * [3]:1 一行
 */
    int out_dim[4];
    //TODO: 9.2.并用 get_tensor_shape 取得 tensor 的shape
    ret = get_tensor_shape(out_tensor, out_dim, 4);
    if(ret <= 0)
    {
        std::cout << "get tensor shape failed, errno: " << get_tengine_errno() << "\n";
        return 1;
    }
    //TODO:9.3 利用get_tensor_buffer 取得输出 tensor 的内容
    float* outdata = ( float* )get_tensor_buffer(out_tensor);

12.利用 输出 和 OpenCV 进行画图

    int num = out_dim[1];
    //设置阈值,是否为检测目标
    float show_threshold = 0.5;
    // 通过 outdata 对 image 进行绘制边框和label信息,并保存
    post_process_ssd(image_file, show_threshold, outdata, num, save_name);

12.程序结束,记得释放内存和资源

    //TODO: 9.4 释放 out_tensor 所占空间
    release_graph_tensor(out_tensor);
    //TODO: 3.1 释放 input_tensor 所占空间
    release_graph_tensor(input_tensor);
    // 释放 graph 执行所占用的资源
    ret = postrun_graph(graph);
    if(ret != 0)
    {
        std::cout << "Postrun graph failed, errno: " << get_tengine_errno() << "\n";
        return 1;
    }
    free(input_data);
    //TODO: 2.2 调用 destroy_graph() 来释放资源
    destroy_graph(graph);
    //TODO: 1.1 释放 tengine
    release_tengine();

任务2:完成

TODO:3.能在 RK3399 上运行该程序。

        RK3399上 tengine 的路径可能会与 笔记本电脑 不同。使用不同的 github 网址,下载下来的 tengine 内容虽然一样,但是 文件名称可能会不一样。比如 tengine-master,tengine,Tengine。(这些都是小细节)
        并且 RK3399 上没有 CLion,所以我们需要手动进行 cmake。
        过程如下:(建议修改前进行 备份 )
1.修改 "CMakeLists.txt" 文件

# 设置 tengine 路径
set( TENGINE_DIR /home/firefly/tengine )

2.创建 build 文件,目的是为了放置 cmake 文件设置错误,删除文件的时候,不确定要删除哪些文件。

1.在 mobilenet_ssd 目录下创建 build 文件
cd tengine/examples/mobilenet_ssd
mkdir build
cd build

2.进行 cmake,前一级目录( cmake 目的是为了生成 make文件)

cmake ..
返回信息
""
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found OpenCV: /usr/local (found version "3.4.2") 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/lee/Documents/tengine/examples/mobilenet_ssd/build
""

3.进行make

make
返回信息
""
Scanning dependencies of target MSSD
[ 33%] Building CXX object CMakeFiles/MSSD.dir/mssd.cpp.o
[ 66%] Building CXX object CMakeFiles/MSSD.dir/home/lee/Documents/tengine/examples/common/common.cpp.o
[100%] Linking CXX executable MSSD
[100%] Built target MSSD
""

4.执行可执行文件

firefly@firefly:~/Tengine/examples/mobilenet_ssd/build$ ./MSSD 
/home/firefly/Tengine/examples/mobilenet_ssd/build/MSSD
proto file not specified,using /home/firefly/Tengine/models/MobileNetSSD_deploy.prototxt by default
model file not specified,using /home/firefly/Tengine/models/MobileNetSSD_deploy.caffemodel by default
image file not specified,using /home/firefly/Tengine/tests/images/ssd_dog.jpg by default
tensor: detection_out created by node: detection_out is not consumed
add the node: detection_out into output list
load model done!
--------------------------------------
repeat 1 times, avg time per run is 197.33 ms
detect result num: 3 
dog :100%
BOX:( 138.509 , 209.394 ),( 324.57 , 541.314 )
car :100%
BOX:( 467.315 , 72.8045 ),( 687.269 , 171.128 )
bicycle :100%
BOX:( 107.395 , 140.657 ),( 574.212 , 415.188 )
======================================
[DETECTED IMAGE SAVED]: [save.jpg](https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxcheckurl?requrl=http%3A%2F%2Fsave.jpg&skey=%40crypt_f9d3b6a2_596aae6997f00ed042e8dccce68cd50a&deviceid=e183140456749306&pass_ticket=OlSyJ0Ia1IhtysLjA0CCvzP6XXavJ0bmawua8PAWQ1GErsg5SDT81873sBxClMpl&opcode=2&scene=1&username=@f7db13e1c47e0fed7e506d9d48739f7e)
======================================
Release Graph Executor for graph graph
Release workspace default resource

打开图片进行检查。(图片在 mobilenet_ssd/build/save.jpg 目录下)


save.jpg

任务3:完成

TODO:4.能在 RK3399 上运行该程序并使用 GPU 加速。

        在 RK3399 上使用 GPU ,在 OAID 上有相关的教程gpu_cpu_mssd
        需要注意的是如果之前已经编译过 tengine ,不能直接使用GPU。需要重新编译(但是我还是失败了)。

1.清除 make 信息
make clean
2.删除 与 驱动相关的文件
rm -r build/driver

我使用GPU时,重新下载 tengine。(即删除原有的 tengine 目录)
下面我从 0 开始讲解如何 设置 RK3399 的 GPU 使用。
1.从 github 中拷贝 ComputeLibrary

git clone https://github.com/ARM-software/ComputeLibrary.git  
git checkout v18.05

2.编译 ComputeLibrary

apt-get install scons
scons Werror=1 -j4 debug=0 asserts=1 neon=0 opencl=1 embed_kernels=1 os=linux arch=arm64-v8a

3.修改 tengine 的 makefile.config 文件

1.使用Arm64
# Set the target arch 
CONFIG_ARCH_ARM64=y
2.使用GPU
# Enable GPU support by Arm Computing Library
CONFIG_ACL_GPU=y
3.不使用BLAS
# Use BLAS as the operator implementation
# CONFIG_ARCH_BLAS=y
4.指定ACL库的路径(注意路径是否正确)
# Set the path of ACL 
ACL_ROOT=/home/firefly/ComputeLibrary
5.使用caffe模型
# Enable other serializers 
CONFIG_CAFFE_SERIALIZER=y

4.进行 make

make

5.进行安装

make install

6.检查

./build/tests/bin/bench_sqz -d acl_opencl

        对我们的 样例 进行修改。
1.修改 "CMakeLists.txt" 文件

cd examples/mobilenet_ssd
leafpad CMakeLists.txt
1.加入 tengine 路径
# 设置 tengine 路径
set( TENGINE_DIR /home/firefly/tengine )

2.添加 build 文件

mkdir build
cd build

3.进行 cmake 编译

cmake ..

4.进行 make 编译

make

5.设置使用 GPU 参数(将GPU频率设置为 800000000)

sudo su
echo "performance" >/sys/devices/platform/ff9a0000.gpu/devfreq/ff9a0000.gpu/governor
cat /sys/devices/platform/ff9a0000.gpu/devfreq/ff9a0000.gpu/cur_freq
命令行输入以下信息(设置临时环境变量)
# 
export GPU_CONCAT=0
# 使能GPU fp16
export ACL_FP16=1
# 设置重复次数
export REPEAT_COUNT=5
# 运行
taskset 0x1 ./MSSD -d acl_opencl

显示如下:

firefly@firefly:~/tengine/examples/mobilenet_ssd/build$ export GPU_CONCAT=0
firefly@firefly:~/tengine/examples/mobilenet_ssd/build$ export ACL_FP16=1
firefly@firefly:~/tengine/examples/mobilenet_ssd/build$ export REPEAT_COUNT=5
firefly@firefly:~/tengine/examples/mobilenet_ssd/build$ taskset 0x1 ./MSSD -d acl_opencl
/home/firefly/tengine/examples/mobilenet_ssd/build/MSSD
proto file not specified,using /home/firefly/tengine/models/MobileNetSSD_deploy.prototxt by default
model file not specified,using /home/firefly/tengine/models/MobileNetSSD_deploy.caffemodel by default
image file not specified,using /home/firefly/tengine/tests/images/ssd_dog.jpg by default
ACL Graph Initialized
Driver: ACLGraph probed 1 devices
tensor: detection_out created by node: detection_out is not consumed
add the node: detection_out into output list
load model done!
--------------------------------------
repeat 5 times, avg time per run is 165.676 ms
detect result num: 3 
dog :100%
BOX:( 138.419 , 209.091 ),( 324.504 , 541.568 )
car :100%
BOX:( 467.356 , 72.9224 ),( 687.269 , 171.123 )
bicycle :100%
BOX:( 107.053 , 140.221 ),( 574.472 , 415.248 )
======================================
[DETECTED IMAGE SAVED]: [save.jpg]
======================================
Release Graph Executor for graph graph
Release workspace default resource

完成任务4。


附比较 RK3399 和 笔记本 时间上的差距

1.RK3399 不使用 GPU 的情况下:

repeat 1 times, avg time per run is 197.33 ms

2.RK3399 使用 GPU的情况下:

repeat 5 times, avg time per run is 165.676 ms

3.笔记本使用 BLAS 库的情况下

repeat 1 times, avg time per run is 133.117 ms

GPU 的使用,对RK3399有明显的加速。
与官方给的 mssd 。


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

推荐阅读更多精彩内容