Electron通過(guò)Napi調(diào)用Tessract實(shí)現(xiàn)文字識(shí)別(含編譯庫(kù)和提升識(shí)別準(zhǔn)確度)

前言

最近做一個(gè)小工具需要用到OCR签钩,一開(kāi)始用的是tesseract.js這個(gè)庫(kù),經(jīng)測(cè)試以后發(fā)現(xiàn)識(shí)別速度實(shí)在太慢凯亮,識(shí)別一張圖片基本都要耗時(shí)幾百毫秒甚至1~2秒边臼,而我的需求對(duì)于檢測(cè)實(shí)時(shí)性比較高,只有另尋他法假消。tesseract.js速度慢可能是因?yàn)樗趙asm移植柠并,性能損耗比較大;之后我嘗試直接使用Tesseract native版本,對(duì)比之下速度快了非常多臼予,于是決定使用node Napi 構(gòu)建本地模塊來(lái)使用tesseract鸣戴。

相關(guān)代碼倉(cāng)庫(kù)在https://github.com/ColorfulHorse/Ruminer,ocr native 模塊在native/ocr目錄下

編譯Tesseract

由于Tesseract并沒(méi)有給我們提供編譯好的動(dòng)態(tài)庫(kù)粘拾,所以需要自己從源碼編譯窄锅,官方文檔建議的三種方式中 Software Network 和 CPPAN 我試過(guò)之后都遇到了一些問(wèn)題,最后使用Vcpkg來(lái)編譯缰雇。其實(shí)也不是非要用包管理器來(lái)幫助編譯入偷,只不過(guò)一般庫(kù)都有一些子依賴,需要依次下載編譯子依賴械哟,不僅麻煩而且容易漏疏之,而包管理器會(huì)自動(dòng)幫我們下載編譯目標(biāo)庫(kù)的子依賴。

Vcpkg是微軟開(kāi)源的一個(gè)c/c++包管理工具暇咆,使用比較簡(jiǎn)單锋爪,參照文檔下載編譯安裝:

git clone https://github.com/microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat

之后將Vcpkg配置到環(huán)境變量方便使用,同時(shí)執(zhí)行vcpkg integrate install將它集成到全局爸业。
運(yùn)行vcpkg search tesseract可以看到Vcpkg已經(jīng)收錄了tesseract庫(kù)其骄,有三個(gè)版本,由于我們不需要訓(xùn)練數(shù)據(jù)也不需要交叉編譯扯旷,直接運(yùn)行vcpkg install tesseract:x64-windows安裝windows 64位版本就可以了(如果需要編譯其他平臺(tái)的版本參照文檔另行修改)拯爽。

庫(kù)版本.png

注意點(diǎn)

Vcpkg初次安裝庫(kù)時(shí)需要下載一些組件,比如cmake薄霜、nuget這些某抓,如果覺(jué)得下載太慢可以set http_proxy=代理設(shè)置臨時(shí)代理,或者自己手動(dòng)復(fù)制鏈接下載相應(yīng)文件復(fù)制到vcpkg\downloads目錄惰瓜。然后編譯安裝這個(gè)過(guò)程也比較慢,等就是了汉矿。

安裝完成以后相關(guān)文件會(huì)在vcpkg\packages\tesseract_x64-windows下崎坊,把需要用到的.lib .dll 以及頭文件拿出來(lái)用就可以了。

構(gòu)建項(xiàng)目

安裝node-gyp

  • 安裝Python洲拇,設(shè)置到環(huán)境變量
  • 安裝Visual Studio或者windows-build-tools奈揍,我這里安裝的是VS2019,推薦安裝VS一勞永逸
  • 安裝node-gyp赋续,npm install -g node-gyp男翰,它的作用相當(dāng)于cmake, 用來(lái)構(gòu)建本地模塊纽乱。

配置本地模塊

1.建立相關(guān)目錄

在項(xiàng)目目錄建立相關(guān)文件夾蛾绎,將tesseract41.lib文件、tesseract頭文件拷貝到目錄中;由于tesseract依賴leptonica庫(kù)進(jìn)行圖片處理租冠,我們也需要用到其中一些函數(shù)鹏倘,所以需要將leptonica的頭文件一并拷貝過(guò)去,所需文件和模塊目錄如圖

tesseractlib.png
tesseract_include.png
leptonica_include.png
dir.png
2. 下載Tesseract語(yǔ)言包

tesseract檢測(cè)不同語(yǔ)言需要相應(yīng)的語(yǔ)言包顽爹,官方提供了一些訓(xùn)練好的語(yǔ)言包纤泵,下載地址在https://github.com/tesseract-ocr/tessdata

將需要用到的語(yǔ)言包下載放入public目錄

tess_data.png
3. 編寫(xiě)binding.gyp配置文件

node-gyp根據(jù)binding.gpy文件來(lái)構(gòu)建c/c++代碼,相當(dāng)于Cmake的CMakeLists镜粤,這里貼一份我的配置捏题,復(fù)制的時(shí)候記得把注釋刪掉

{
  'targets': [
    {
       # 模塊名稱
      'target_name': 'ocr',
      "cflags!": [ "-fno-exceptions", "-fPIC" ],
      "cflags_cc!": [ "-fno-exceptions", "-fPIC" ],
      "ldflags": [
        # 將當(dāng)前目錄加入到動(dòng)態(tài)庫(kù)搜索路徑,打包時(shí)將需要.dll文件全部拷貝到exe文件所在目錄
        "-Wl,-rpath,'$$ORIGIN'"
      ],
      "sources": [
          # 需要編譯的源文件
          'src/lib.cpp',
          'src/index.cpp'
      ],
      "include_dirs": [
        # 頭文件目錄
        "<!@(node -p \"require('node-addon-api').include\")",
        "src/include"
      ],
      "dependencies":[
        # node本身需要的依賴
        "<!(node -p \"require('node-addon-api').gyp\")"
      ],
      'defines': [
        'NAPI_DISABLE_CPP_EXCEPTIONS'
      ],
      "conditions": [
        ["OS=='win'",
          {
            "libraries": [
              # 需要鏈接的依賴庫(kù)肉渴,我這里另外用到了opencv做前處理
              "-l<(module_root_dir)/libs/tesseract41.lib",
              "-l<(module_root_dir)/libs/opencv_imgproc.lib",
              "-l<(module_root_dir)/libs/opencv_features2d.lib"
            ]
          }
        ]
      ]
    }
  ]
}

編寫(xiě)代碼調(diào)用tesseract

我這邊的圖像源來(lái)自屏幕錄制涉馅,每幾百毫秒捕獲一次屏幕截圖,轉(zhuǎn)成base64字符串丟到c++層調(diào)用tesseract進(jìn)行識(shí)別黄虱,然后回傳結(jié)果稚矿,代碼如下。

lib.h

#ifndef OCR_LIB_H
#define OCR_LIB_H

#include "include/tesseract/baseapi.h"
#include "include/leptonica/allheaders.h"
#include <string>

using namespace std;
using namespace tesseract;

void init(string path);

int loadLanguage(string lang);

string recognize(string base64);

void destroy();

#endif

lib.cpp

#include "include/lib.h"

TessBaseAPI *api = nullptr;
string dataPath = "";

// 初始化
void init(string path) {
    if (api == nullptr) {
        api = new TessBaseAPI();
    }
    dataPath = path;
}

// 加載語(yǔ)言包
int loadLanguage(string lang) {
    return api->Init(dataPath.c_str(), lang.c_str());
}

// 檢測(cè)圖片中的文字
string recognize(string base64) {
    l_int32 size;
    l_uint8* source = decodeBase64(base64.c_str(), strlen(base64.c_str()), &size);
    PIX * pix = pixReadMem(source, size);
    lept_free(source);
    api->SetImage(pix);
    api->SetSourceResolution(96);
    string result = api->GetUTF8Text();
    pixDestroy(&pix);
    return result;
}

void destroy() {
    if (api != nullptr) {
        api->End();
        delete api;
        api = nullptr;
    }
    dataPath = "";
}

index.cpp 這里用來(lái)暴露接口提供給js層

#include <napi.h>
#include <string>
#include "include/lib.h"

using namespace Napi;

void Initialize(const CallbackInfo& info) {
    Env env = info.Env();
    init(info[0].ToString());
}

Number LoadLang(const CallbackInfo& info) {
    Env env = info.Env();
    int ret = loadLanguage(info[0].ToString());
    return Napi::Number::New(env, ret);
}

String Recognize(const CallbackInfo& info) {
    Env env = info.Env();
    string text = recognize(info[0].ToString());
    String res = Napi::String::New(env, text);
    delete[] text.c_str();
    return res;
}

void Destroy(const CallbackInfo& info) {
    destroy();
}

// 設(shè)置類(lèi)似于 exports = {key:value}的模塊導(dǎo)出
Object Init(Env env, Object exports) {
    exports["init"] = Function::New(env, Initialize);
    exports["loadLanguage"] = Function::New(env, LoadLang);
    exports["recognize"] = Function::New(env, Recognize);
    exports["destroy"] = Function::New(env, Destroy);
    return exports;
}

NODE_API_MODULE(ocr, Init)

編譯生成庫(kù)文件

寫(xiě)好邏輯代碼之后打開(kāi)命令行切換路徑到binding.gyp文字所在目錄捻浦,執(zhí)行node-gyp configure晤揣, node-gyp build;順利的話將會(huì)在build/Release目錄下生成ocr.node庫(kù)文件朱灿,而它需要依賴的dll文件vcpkg也會(huì)幫我們拷貝過(guò)來(lái)昧识,非常方便。

ocr.node.png

編寫(xiě)js/ts文件調(diào)用本地模塊

index.ts

import { loadAddonFile } from '@/utils/NativeUtil'

const ocr = loadAddonFile('src/native/ocr/build/Release/ocr.node', 'ocr.node')

export default {
  init: (langPath: string) => {
    ocr.init(langPath)
  },
  loadLanguage: (lang: string): number => {
    return ocr.loadLanguage(lang)
  },
  recognize: (base64: string): Array<string> => {
    return ocr.recognize(base64)
  },
  destroy: () => {
    return ocr.destroy()
  }
}

注意點(diǎn)

如何使用本地模塊盗扒,正常情況直接`require(xxx.node)就可以了跪楞,由于我的項(xiàng)目使用vue-cli-plugin-electron-builder構(gòu)建,所以導(dǎo)入時(shí)要做一些路徑處理

import path from 'path'

declare const __non_webpack_require__: any
export function loadAddonFile(devSrc: string, productSrc: string) {
  if (process.env.NODE_ENV !== 'production') {
     // 開(kāi)發(fā)環(huán)境從項(xiàng)目目錄導(dǎo)入
    // eslint-disable-next-line
    return __non_webpack_require__(path.join(process.cwd(), devSrc))
  } else {
     // 生產(chǎn)環(huán)境從打包根目錄導(dǎo)入
    // eslint-disable-next-line
    return __non_webpack_require__(path.join(process.resourcesPath, '../' + productSrc))
  }
}

另外還要將.dll文件拷貝到打包后生成的.exe文件目錄下侣灶,否則會(huì)找不到庫(kù)甸祭,electron-builder配置如下

builderOptions: {
        productName: 'xxx',
        appId: 'xxx',
        copyright: 'xxx',
        extraFiles: [
          {
            // 拷貝dll庫(kù)文件到打包根目錄
            from: 'src/native/ocr/build/Release',
            to: '.'
          }
        ]
}

一切完成后就可以使用測(cè)試一下是否能夠正常運(yùn)行了,如果不正常大概率是導(dǎo)入路徑不正確或者缺少一些dll庫(kù)文件褥影。

提升識(shí)別準(zhǔn)確度

經(jīng)過(guò)一些測(cè)試發(fā)現(xiàn)池户,tesseract基本上只能識(shí)別出文字顏色和背景顏色有明顯區(qū)別,而且背景顏色比較單一的圖片凡怎,例如白底黑字黑底白字這種校焦,我猜測(cè)它對(duì)于圖片只是做了簡(jiǎn)單的二值化。但是我的需求比較復(fù)雜一點(diǎn)统倒,有時(shí)候文字背景比較復(fù)雜寨典,背景和文字顏色差別也不是很明顯,所以需要對(duì)圖片做一些前處理提升準(zhǔn)確度房匆。這里使用OpenCV來(lái)做一些簡(jiǎn)單的處理耸成,先使用MSER算法配合一些形態(tài)學(xué)操作檢測(cè)文本區(qū)域报亩,然后裁剪文字區(qū)域進(jìn)行Otsu二值化,最后丟給tesseract進(jìn)行檢測(cè)墓猎。

基本流程

  • 原圖轉(zhuǎn)換為灰度圖
  • 對(duì)灰度圖做MSER+和MSER-
  • 將MSER+和MSER-檢測(cè)到的區(qū)域填充成白色
  • 將MSER+和MSER-的結(jié)果圖取交集
  • 交集圖做MORPH_CLOSE閉運(yùn)算操作捆昏,消除鄰近區(qū)域之間的空隙
  • 查找區(qū)域輪廓,將原圖中符合要求的區(qū)域裁剪出來(lái)

修改后的代碼如下
lib.h

#ifndef OCR_LIB_H
#define OCR_LIB_H

#include "include/tesseract/baseapi.h"
#include "include/leptonica/allheaders.h"
#include <string>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace tesseract;

void init(string path);

int loadLanguage(string lang);

vector<string> recognize(string base64);

void destroy();

cv::Mat pixToMat(Pix *pix);

Pix *mat8ToPix(cv::Mat *mat8);

static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";

std::string base64_decode(std::string const& encoded_string);

std::vector<cv::Rect> getRect(cv::Mat srcImage);
#endif

lib.cpp

#include "include/lib.h"

TessBaseAPI *api = nullptr;
string dataPath = "";

void init(string path) {
    if (api == nullptr) {
        api = new TessBaseAPI();
    }
    dataPath = path;
}

int loadLanguage(string lang) {
    int ret = api->Init(dataPath.c_str(), lang.c_str(), OEM_LSTM_ONLY);;
    return ret;
}

vector<string> recognize(string base64) {
    string decoded_string = base64_decode(base64);
    vector<uchar> data(decoded_string.begin(), decoded_string.end());
    cv::Mat img = cv::imdecode(data, cv::IMREAD_UNCHANGED);
    cv::Mat gray;
    cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
    vector<cv::Rect> rects = getRect(gray);
    vector<string> res;
    for (size_t i = 0; i < rects.size(); i++) {
        cv::Rect rect = rects[i];
        cv::Mat area(gray, rect);
        // 二值化
        cv::threshold(area, area, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
        PIX * pix = mat8ToPix(&area);
        api->SetImage(pix);
        api->SetSourceResolution(96);
        string result = api->GetUTF8Text();
        pixDestroy(&pix);
        res.push_back(result);
    }
    return res;
}

void destroy() {
    if (api != nullptr) {
        api->End();
        delete api;
        api = nullptr;
    }
    dataPath = "";
}

/**
 * Mat灰度圖轉(zhuǎn)Pix
 */
Pix *mat8ToPix(cv::Mat *mat8) {
    Pix *pixd = pixCreate(mat8->size().width, mat8->size().height, 8);
    for(int y=0; y<mat8->rows; y++) {
        for(int x=0; x<mat8->cols; x++) {
            pixSetPixel(pixd, x, y, (l_uint32) mat8->at<uchar>(y,x));
        }
    }
    return pixd;
}

static inline bool is_base64(unsigned char c) {
    return (isalnum(c) || (c == '+') || (c == '/'));
}

/**
 * base64解碼
 */
std::string base64_decode(std::string const& encoded_string) {
    int in_len = encoded_string.size();
    int i = 0;
    int j = 0;
    int in_ = 0;
    unsigned char char_array_4[4], char_array_3[3];
    std::string ret;
    while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
        char_array_4[i++] = encoded_string[in_]; in_++;
        if (i == 4) {
            for (i = 0; i < 4; i++)
                char_array_4[i] = base64_chars.find(char_array_4[i]);
            char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
            char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
            char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
            for (i = 0; (i < 3); i++)
                ret += char_array_3[i];
            i = 0;
        }
    }
    if (i) {
        for (j = i; j < 4; j++)
            char_array_4[j] = 0;
        for (j = 0; j < 4; j++)
            char_array_4[j] = base64_chars.find(char_array_4[j]);

        char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
        char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
        char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
        for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
    }
    return ret;
}

/**
 * 檢測(cè)文本區(qū)域
 */
std::vector<cv::Rect> getRect(cv::Mat gray) {
    cv::Mat gray_neg;
    // 取反值灰度
    gray_neg = 255 - gray;
    std::vector<vector<cv::Point> > regContours;
    std::vector<vector<cv::Point> > charContours;//點(diǎn)集

    // 創(chuàng)建MSER對(duì)象
    // _max_variation 最大變化率大于此值的將被忽略
    // _min_diversity 兩個(gè)區(qū)域的區(qū)別小于此值將被忽略
    cv::Ptr<cv::MSER> mesr1 = cv::MSER::create(5, 20, 5000, 0.5, 0.3);
    cv::Ptr<cv::MSER> mesr2 = cv::MSER::create(5, 20, 400, 0.1, 0.3);
    std::vector<cv::Rect> bboxes1;
    std::vector<cv::Rect> bboxes2;
    // MSER+ 檢測(cè)
    mesr1->detectRegions(gray, regContours, bboxes1);
    // MSER-操作
    mesr2->detectRegions(gray_neg, charContours, bboxes2);

    cv::Mat mserMapMat = cv::Mat::zeros(gray.size(), CV_8UC1);
    cv::Mat mserNegMapMat = cv::Mat::zeros(gray.size(), CV_8UC1);

    for (size_t i = 1; i < regContours.size(); i++) {
        // 根據(jù)檢測(cè)區(qū)域點(diǎn)生成mser+結(jié)果
        const std::vector<cv::Point>& r = regContours[i];
        for (size_t j = 0; j < r.size(); j++) {
            cv::Point pt = r[j];
            mserMapMat.at<unsigned char>(pt) = 255;
        }
    }
    //MSER- 檢測(cè)
    for (size_t i = 1; i < charContours.size(); i++) {
        // 根據(jù)檢測(cè)區(qū)域點(diǎn)生成mser-結(jié)果
        const std::vector<cv::Point>& r = charContours[i];
        for (size_t j = 0; j < r.size(); j++) {
            cv::Point pt = r[j];
            mserNegMapMat.at<unsigned char>(pt) = 255;
        }
    }
    cv::Mat mserResMat;
    mserResMat = mserMapMat;
    mserResMat = mserMapMat & mserNegMapMat;    // mser+與mser-位與操作
    // 開(kāi)運(yùn)算
    cv::Mat mserClosedMat;
    cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(20, 20));
    cv::morphologyEx(mserResMat, mserClosedMat,
        cv::MORPH_CLOSE, kernel);
    // 尋找外部輪廓
    std::vector<std::vector<cv::Point> > plate_contours;
    cv::findContours(mserClosedMat, plate_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
    // 候選區(qū)域判斷輸出
    std::vector<cv::Rect> candidates;
    for (size_t i = 0; i != plate_contours.size(); ++i) {
        // 求解最小外界矩形
        cv::Rect rect = cv::boundingRect(plate_contours[i]);
        // 寬高比例
        double wh_ratio = rect.width / double(rect.height);
        if (wh_ratio > 0.5) {
            // 忽略太小的區(qū)域
            if (rect.width > 50) {
                // 區(qū)域加一些間隔以免字符不完整
                const int margin = 5;
                int l = rect.x - margin < 0 ? 0 : rect.x - margin;
                int t = rect.y - margin < 0 ? 0 : rect.y - margin;
                int r = l + rect.width + margin > gray.cols ? gray.cols : l + rect.width + margin;
                int b = t + rect.height + margin > gray.rows ? gray.rows : t + rect.height + margin;
                cv::Rect rec(l, t, r - l, b - t);
                candidates.push_back(rec);
            }
        }
    }
    return candidates;
}

index.cpp導(dǎo)出函數(shù)也要做一些修改

Array Recognize(const CallbackInfo& info) {
    Env env = info.Env();
    vector<string> textList = recognize(info[0].ToString());
    Array array = Array::New(env);
    for (size_t idx = 0; idx < textList.size(); idx++) {
        // The HandleScope is recommended especially when the loop has many
        // iterations.
        Napi::HandleScope scope(env);
        array[idx] = Napi::String::New(env, textList[idx]);
    }
    return array;
}

加入這些優(yōu)化之后在一定程度上提升了文字識(shí)別的準(zhǔn)確率,但是如果用于識(shí)別自然場(chǎng)景中的文字恐怕還是無(wú)能為力,這就要涉及到深度學(xué)習(xí)領(lǐng)域了革半,不在本文討論范圍內(nèi)讹剔。

結(jié)語(yǔ)

在electron中構(gòu)建node本地模塊還是有不少坑的,一方面對(duì)windows的庫(kù)鏈接不太熟悉,gyp也不熟悉,一方面node-addon的文檔也不太容易讀,浪費(fèi)了不少時(shí)間遍烦,對(duì)一個(gè)半吊子來(lái)說(shuō)也可以了;好在最后基本把設(shè)想的東西都完成了躺枕,收獲也很多服猪。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拐云,隨后出現(xiàn)的幾起案子罢猪,更是在濱河造成了極大的恐慌,老刑警劉巖叉瘩,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件膳帕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡薇缅,警方通過(guò)查閱死者的電腦和手機(jī)危彩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)泳桦,“玉大人汤徽,你說(shuō)我怎么就攤上這事∨钛鳎” “怎么了泻骤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)梧奢。 經(jīng)常有香客問(wèn)我,道長(zhǎng)演痒,這世上最難降的妖魔是什么亲轨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮鸟顺,結(jié)果婚禮上惦蚊,老公的妹妹穿的比我還像新娘器虾。我一直安慰自己,他們只是感情好蹦锋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布兆沙。 她就那樣靜靜地躺著,像睡著了一般莉掂。 火紅的嫁衣襯著肌膚如雪葛圃。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天憎妙,我揣著相機(jī)與錄音库正,去河邊找鬼。 笑死厘唾,一個(gè)胖子當(dāng)著我的面吹牛褥符,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播抚垃,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼喷楣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了鹤树?” 一聲冷哼從身側(cè)響起铣焊,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎魂迄,沒(méi)想到半個(gè)月后粗截,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捣炬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年熊昌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片湿酸。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡婿屹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出推溃,到底是詐尸還是另有隱情昂利,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布铁坎,位于F島的核電站蜂奸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏硬萍。R本人自食惡果不足惜扩所,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望朴乖。 院中可真熱鬧祖屏,春花似錦助赞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至期丰,卻和暖如春群叶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咐汞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工盖呼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人化撕。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓几晤,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親植阴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蟹瘾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容