Java + OpenCV 实现银行卡号识别 (2)

字符分割

这一篇主要讨论如何利用图像处理方法分割银行卡号字符。

由上一篇的结果我们可以得到完整银行卡号条目的图像,假如不加入序列识别能力的神经网络 (LSTM 等),那么就需要对这一卡号字符序列进行切分,再使用一般的分类器识别出单个数字 (0-9)。但这里包含两种类型的字体,凹凸卡号代表的是 Farrington-7B,是用于信用卡的一种特殊字体;平面卡号是普通的印刷体。所以分割得到单个图像数字后,要提供这两种类型的图像数字训练数据给分类器做训练,这一内容放到下一篇 (3) 去讲。

给出我们的卡号定位原图


定位的卡号条目.png
字符分割前的预处理

字符分割难就难在凹凸浮雕类型字符的分割,如果是平面印刷字符可以用局部或固定阈值法处理,效果很好,背景噪声几乎没有,不论光照强弱。

下图展示了采用全局固定阈值法进行二值化后的效果。


印刷卡号-1.png

而浮雕类的卡号效果就天壤之别了..对比一下


浮雕卡号-1.png

所以对于浮雕字体我采用了特殊的处理方法,是他们尽量变得粗大,至于粘合的字符,后面可以使用相对应算法作分割。

  1. 同样的,灰度化,简化图像信息,得到 dst。

  2. 由 dst 依次通过 Top-hat 变换、形态学梯度、OTSU 二值化,得到 dst0。

  3. 由 dst 依次通过 Black-hat 变换、形态学梯度、中值滤波、OTSU 二值化得到 dst1。

  4. dst0 与 dst1 都是 0 或 1 二值图像,每一点像素做或预算(加法),得到 dst2。

  5. 最后 dst2 执行闭操作和膨胀处理。

以上代码

public class CVRegion extends ImgSeparator {

    ....

    private CardFonts.FontType fontType;

    ....

    public void digitSeparate() throws Exception {
        Mat binDigits = new Mat(grayMat, getRectOfDigitRow()).clone();
        /*** CardFonts 判断字体类型
        *  包含三种 UNKNOWN, LIGHT_FONT, BLACK_FONT
        */
        CardFonts fonts = CVFontType.getFontType(binDigits);
        CardFonts.FontType type = fonts.getType();

        // 浮雕类型字体
        if (type == CardFonts.FontType.LIGHT_FONT) {
            Mat sqKernel = Imgproc.getStructuringElement(
                                    Imgproc.MORPH_RECT, new Size(5, 5));

            Mat dst0 = new Mat();
            Imgproc.morphologyEx(binDigits, dst0, Imgproc.MORPH_TOPHAT, 
                                                            sqKernel);

            Imgproc.morphologyEx(dst0, dst0, Imgproc.MORPH_GRADIENT, 
                                                            sqKernel);

            Imgproc.threshold(dst0, dst0, 0, 255, Imgproc.THRESH_BINARY | 
                                                      Imgproc.THRESH_OTSU);

            Imgproc.medianBlur(dst0, dst0, 3);

            Mat dst1 = new Mat();
            Imgproc.morphologyEx(binDigits, dst1, Imgproc.MORPH_BLACKHAT, 
                                                            sqKernel);
            Imgproc.morphologyEx(dst1, dst1, Imgproc.MORPH_GRADIENT, 
                                                            sqKernel);
            Imgproc.medianBlur(dst1, dst1, 3);
            Imgproc.threshold(dst1, dst1, 0, 255, Imgproc.THRESH_BINARY | 
                                                      Imgproc.THRESH_OTSU);

            Core.bitwise_or(dst0, dst1, dst1);

            Imgproc.morphologyEx(dst1, dst1, Imgproc.MORPH_CLOSE, 
        Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)));

            Imgproc.dilate(dst1, binDigits, 
        Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(1, 5)));

        }

        if (type == CardFonts.FontType.BLACK_FONT || 
                                    type == CardFonts.FontType.UNKNOWN) {
            binDigits = fonts.getFonts();
        }
        this.binDigitRegion = binDigits;
        this.fontType = type;
        
        // 先忽略
        setSingleDigits();
    }
}

利用 Top-hat 和 Black-hat 变换可以突出图中较亮和较暗区域的特征,这使得浮雕数字的受光侧和背光侧在梯度变换中能被更好地感知,增强其边缘特征,从而降低了背景纹理噪声的影响。

得到边缘轮廓的特征就能描述字符所在区域了.这时候使用阈值分割算法对图像进行二值化,采用固定阈值法还是局部阈值化方法这是一个问题,为了对比它们之间的效果,我在这里放了两张图。


局部阈值.png

固定阈值.png

比较 adaptive 和 OTSU 可以看出,虽然 adaptive 能将字符较完整地描绘出来,但引入了许多椒盐噪点,即使使用滤波器也不能很好地去除。这会给我后面的字符切割增加很多困难,因为这样的背景噪声是千变万化的,它们干扰了算法对字符区域的判断。

OTSU 的效果很好,几乎可以除去背景噪声的干扰。数字没有被完整刻画出来没有关系,我们需要的是它的大概位置,只要给出部分信息即可。但同样可以在这基础上进行强化,也就是增加较暗区域特征,再利用二值图加法操作对其进行”补充“。

增强后的数字特征更为完整,再利用膨胀操作可以减少字符的断裂,避免一个数字一分为二,对分割造成困扰。


bitwise_or.png
分割算法
  • 竖直坐标投影法
    坐标投影法是众多字符分割研究方法中较为简单高效的一种。它主要用于二值图中字符无粘连或粘连部位较薄弱、背景噪声较细微的场景。

该方法将二值图同一纵坐标上白色像素点进行累加,x方向上逐个记录累计值,用公式表示为:

P_x = \Sigma^H_{y=0}G(x, y)
其中 H 为图像高度,G(x, y) 代表 (x, y) 处像素值.

这样一来就构成了横坐标位置-纵坐标白色像素数目的二维对应关系图,也即投影的散点图。在噪声干扰较小或字符粘连较轻微的情况下,字符区域的白色像素会明显多于字符间隙中的白色像素。

这投射到散点图上便会形成波峰与波谷。波谷区域就是字符间的空隙区,相邻的波谷间为单个字符区域。但若是字符间的间隙存在过大的斑点噪声,或字符间粘连较重,投影图中将难以分辨波峰与波谷,也即无法分辨出单个字符区域与字符的间隙区域。所以,单纯的坐标投影对银行卡号分割,特别是凹凸类字符的分割效果十分不理想。

  • 连通域分割法
    通过标记二值图中的连通域,提取其外接矩形轮廓,便能分离出单个卡号字符。如果出现的噪声都是远小于字符的椒盐噪声,就可以在提取矩形轮廓后依靠矩形宽高加以区分。


    连通域提取.png
  • 难点分析
    图像中的数字偶尔会发生断裂的情况,这些算法处理不了断裂的字符。断裂使得算法将字符的一小部分当作成一个整体来看,这是不合理的。其次还有字符粘连的问题,将多个字符当作成一个整体。

  • 改进算法
    为了解决断裂和粘连问题,可以利用卡号数字大小一致的特点做判断和整合。
    如果有先验条件,已知字体的宽度,那么遇到宽度过小的区域,可以认为是断裂造成的,将其左右部分结合,整合后的区域若宽度在指定范围内,则完成该字符的分割。这里称之为窗口分割。

窗口的宽度大小取决于连通域的宽度中方差最小的一组宽度的平均值(最小方差法),设为字体的已知宽度。在完成一个字符的分割后,向右移动继续进行后续的分割。在所有可行的整合方案内,找到宽度方差最小的作为最终的字符整合方案。这就解决了断裂问题。

至于粘连问题也是如法炮制。如果宽度过大,则在字符内的合理区间中找到最薄弱的位置进行左右切分,将粘连字符一分为二。这里不采用最优解。


断裂的字符.png
  • 代码实现
public class CVRegion extends ImgSeparator {

....

    public void setSingleDigits() throws Exception {
        if (fontType == CardFonts.FontType.BLACK_FONT) {
            // 印刷类字体不需要使用窗口分割法
            return;
        }
        // 垂直坐标投影,获得每一个 x 坐标映射数量
        int []x = calcHistOfXY(binDigitRegion, true);
        // 添加每一个 x 坐标作链表节点
        int cur = 0;
        List<Integer> cutting = new LinkedList<>();
        if (x[cur] > 0)
            cutting.add(cur);
        while (true) {
            int next = findNext(x, cur);
            if (next >= x.length)
                break;
            cutting.add(next);
            cur = next;
        }
        // 最小方差法获得窗口宽度
        int ref = getDigitWidth(cutting);
        if (ref < 0)
            return;
        
        SplitList splitList = new SplitList(cutting, ref);
        /**
        * 先分割粘连字符,后合并断裂字符
        */
        split(splitList);
        final int upperWidth = (int)(1.2f * ref);
        final int lowerWidth = (int)(0.6f * ref);
        // remove Node that is a complete digit before merging
        SplitList output = splitList.out(upperWidth, lowerWidth);
        // crack into several fragment to merge into a complete digit
        List<SplitList> buckets = splitList.crack(upperWidth);
        for (SplitList elem : buckets) {
            // 整合单一字符
            merge(elem);
            output.addAll(elem.toNodeList());
        }
        // sort Nodes by its id, ensure the origin order of card numbers
        output.sort();
        /**
          定位到每个数字所在位置后,将它们拷贝到新 Mat 数组中存放
        */
        paintDigits(output.toSimpleList());
    }

    public []int calcHistOfXY(Mat m, boolean axisX) {
        int []calc;
        int rows = m.rows();
        int cols = m.cols();
        byte buff[] = new byte[rows * cols];
        m.get(0, 0, buff);
        if (axisX) {
            calc = new int[cols];
            for (int i = 0; i < cols; i++) {
                for (int j = 0; j < rows; j++)
                    calc[i] += (buff[i + j * cols] & 0x1);
            }
        } else {
            calc = new int[rows];
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++)
                    calc[i] += (buff[i * cols + j] & 0x1);
            }
        }
        return calc;
    }

    // 分割粘连的字符
    public void split(SplitList splitList) {
        int rows = binDigitRegion.rows();
        int cols = binDigitRegion.cols();
        byte buff[] = new byte[cols * rows];
        binDigitRegion.get(0, 0, buff);
        int upperWidth = (int)(1.38f * splitList.getStandardWidth());
        int lowerWidth = (int)(0.8f * splitList.getStandardWidth());
        int window = upperWidth - lowerWidth;
        for (int i = 0; i < splitList.size(); i++) {
            SplitList.Node node = splitList.get(i);
            if (node.width() > upperWidth) {
                int x = node.getStartPointX() + lowerWidth;
                int spx = splitX(buff, x, x + window);
                if (spx > 0) {
                    splitList.split(i, spx);
                }
            }
        }
    }

    public void merge(SplitList splitList) throws Exception {
        int min = Integer.MAX_VALUE;
        String solution = "";
        System.err.println("merge size: " + splitList.size());
        if (splitList.size() > 10) {
            throw new Exception("CVRegion error: splitList.size() is too large and over time limit to merge in function merge(SplitList spl).");

        }
        List<String> box = new ArrayList<>();
        permutations(splitList.size(), 0, "", box);
        for (int i = 0; i < box.size(); i++) {
            String s = box.get(i);
            int splIndex = 0;
            int score = 0;
            for (int j = 0; j < s.length(); j++) {
                int val = s.charAt(j) - '0';
                int distance = splitList.dist(splIndex, splIndex +val -1);
                splIndex += val;
                score += Math.abs(distance -splitList.getStandardWidth());
            }
            if (score < min) {
                min = score;
                solution = s;
            }
        }
        for (int c = 0, spl = 0; c < solution.length(); c++) {
            int val = solution.charAt(c) - '0';
            splitList.join(spl, spl + val - 1);
            spl++;
        }
    }

    private int splitX(byte []buff, int si, int ei) {
        int max = 0;
        int index = 0;
        int rows = binDigitRegion.rows();
        int cols = binDigitRegion.cols();
        for (int x = si; x <= ei; x++) {
            int len = 0;
            for (int y = 0; y < rows; y++)
                if (buff[y * cols + x] == 0)
                    len++;
            if (max < len) {
                max = len;
                index = x;
            }
        }
        return index;
    }
....

}

上面 merge 方法中的 permutations 我要说明一下,这是一个排列组合的做法,也就是说现在需要找到合理的合并方式,现在有 n 个节点(字符块),需要找到它们所有的组合方式。当然,只有是相邻的节点之间能组合在一起。

但这样枚举的话复杂度很高,需要进行剪枝。我们考虑字符最多断裂成 3 块,如果真的出现,其实也代表着二值化效果很差,继续处理没有什么意义,当然这样的情况很罕见。所以,不考虑合并 >3 块的情况,这样可以大大缩小解空间。

    private void permutations(int total, int n, String solution, List<String> box) {
        if (total < n)
            return;
        if (total == n) {
            box.add(solution.substring(1) + n);
            return;
        }
        solution += n;
        permutations(total - n, 3, solution, box);
        permutations(total - n, 2, solution, box);
        permutations(total - n, 1, solution, box);
    }

继续给出前面涉及的方法代码

public abstract class ImgSeparator implements RectSeparator, DigitSeparator{

....

    protected int findNext(int v[], int index) {
        int i;
        boolean get = false;
        if (index < v.length && v[index] != 0)
            get = true;
        for (i = index; i < v.length; i++) {
            if (get && v[i] == 0)
                break;
            if (!get && v[i] != 0)
                break;
        }
        return i;
    }

    /**
     * descending sort
     * @param a array with index and value
     */
    private void sortMap(int [][]a) {
        for (int i = 0; i < a[1].length - 1; i++) {
            int k = i;
            for (int j = i + 1; j < a[1].length; j++) {
                if (a[1][k] < a[1][j]) {
                    k = j;
                }
            }
            if (k != i) {
                swap(a[0], i, k);
                swap(a[1], i, k);
            }
        }
    }

    /**
     * get the average width of id region digits
     * @param cutting
     * @return
     */
    protected int getDigitWidth(List<Integer> cutting) throws Exception {
        if ((cutting.size() & 0x1) == 1) {
            System.err.println("ImgSeparator error: cutting.size() cannot be odd number in function getDigitWidth(List<Integer> c");

            cutting.remove(cutting.size() - 1);
        }

        final int window = 5;
        int [][]width = new int[2][cutting.size() >> 1];
        if (width[0].length <= window) {
            return -1;
        }
        for (int i = 1, j = 0; i < cutting.size(); i+= 2, j++) {
            width[1][j] = cutting.get(i) - cutting.get(i - 1);
            width[0][j] = j;
        }

        sortMap(width);
        int ms = -1;
        float m = Float.MAX_VALUE;
        int sum = 0;
        for (int i = 0; i < window; i++)
            sum += width[1][i];
        for (int i = window; i < width[0].length; i++) {
            float diff = 0;
            if (i > window)
                sum += (- width[1][i - window - 1] + width[1][i]);
            float avg = sum / window;
            for (int j = 0; j < window; j++) {
                diff += Math.pow(width[1][i - j] - avg, 2);
            }
            // get the min square difference
            if (diff < m) {
                ms = i - window;
                m = diff;
            }
        }

        int corrWidth = 0;
        for (int i = window; i > 0; i--)
            corrWidth += width[1][ms + i - 1];
        return corrWidth / window;
    }

    protected void paintDigits(List<Integer> cuttingList) {
        for (int i = 1; i < cuttingList.size(); i++) {
            if ((i & 0x1) == 0)
                continue;
            int x1 = cuttingList.get(i - 1);
            int x2 = cuttingList.get(i);
            Mat crop = new Mat(grayMat, new Rect(x1 + rectOfDigitRow.x, 
                      rectOfDigitRow.y, x2 - x1, rectOfDigitRow.height));

            byte buff[] = new byte[crop.rows() * crop.cols()];
            crop.get(0, 0, buff);
            Mat dst = Mat.zeros(new Size(rectOfDigitRow.width, 
                                  rectOfDigitRow.height), grayMat.type());

            byte out[] = new byte[dst.cols() * dst.rows()];
            for (int j = 0; j < crop.rows(); j++)
                System.arraycopy(buff, j * crop.cols(), out, j *dst.cols(),
                                        crop.cols());

            dst.put(0, 0, out);
            dst = CVGrayTransfer.resizeMat(dst, 380);
            matListOfDigit.add(dst);
        }
    }

....

}

这里实现了特殊的数据结构链表 SplitList,用于断裂和合并节点。

import java.util.*;

/**
 * Created by chenqiu on 3/9/19.
 */
public class SplitList {

    private List<Node> sList;

    private int standardWidth;

    public SplitList(){
        this.sList = new LinkedList<>();
        standardWidth = 0;
    }

    public SplitList(List<Integer> cutting, int standardWidth) {
        this.sList = new LinkedList<>();
        int id = 0;
        for (int i = 0; i < cutting.size(); i += 2) {
            Node n = new Node(id, cutting.get(i), cutting.get(i + 1));
            id += 20;
            sList.add(n);
        }
        this.standardWidth = standardWidth;
    }

    public SplitList(SplitList copy, int start, int end) {
        this.sList = new LinkedList<>();
        for (; start <= end; start++) {
            Node n = copy.get(start).clone();
            this.sList.add(n);
        }
        this.standardWidth = copy.standardWidth;
    }

    public int getStandardWidth() {
        return standardWidth;
    }

    public class Node {
        private int id;
        private int width;
        private int x1;
        private int x2;
        public Node(int id, int x1, int x2) {
            this.id = id;
            this.x1 = x1;
            this.x2 = x2;
            this.width = x2 - x1;
        }

        public int width() {
            return width;
        }

        public int getStartPointX() {
            return x1;
        }

        public int getEndPointX() {
            return x2;
        }

        public int id() {
            return id;
        }

        public Node clone() {
            return new Node(this.id, this.x1, this.x2);
        }

        public String toString() {
            return "[Node: [id: " + id + ", x1: " + x1 + ", x2: " + x2 + 
                   "]]";
        }
    }

    /**
     * division of itself
     * @param gap
     * @return
     */
    public List<SplitList> crack(int gap) {
        List<SplitList> rstSet = new ArrayList<>(30);
        Node cur, next = sList.get(0);
        int start = 0;
        for (int i = 0; i < sList.size() - 1; i++) {
            cur = next;
            next = sList.get(i + 1);
            if (next.x2 - cur.x1 > gap) {
                rstSet.add(rstSet.size(), new SplitList(this, start, i));
                start = i + 1;
            }
        }

        rstSet.add(rstSet.size(),new SplitList(this,start,sList.size()-1));
        this.sList = null; // release this
        return rstSet;
    }

    /**
     * split the sticky digits character in one Node
     * @throws Exception
     */
    public void split(int index, int x){
        Node loc = sList.get(index);
        sList.add(index + 1, new Node(loc.id + 1, x + 1, loc.x2));
        loc.x2 = x;
        // update width
        loc.width = loc.x2 - loc.x1;
    }

    public void join(int si, int ei) {
        if (si >= ei)
            return;
        Node prev = sList.get(si);
        Node last = null;
        int count = ei - si;
        for (int i = 0; i < count; i++) {
            last = sList.remove(si + 1);
        }
        prev.x2 = last.x2;
        prev.width = prev.x2 - prev.x1;
    }

    public int size() {
        return sList.size();
    }

    public void sort() {
        Collections.sort(this.sList, new Comparator<Node>() {
            @Override
            public int compare(Node o1, Node o2) {
                return o1.id - o2.id;
            }
        });
    }

    public Node get(int index) {
        return sList.get(index);
    }

    public void addAll(List<Node> nodeList) {
        for (Node node : nodeList) {
            sList.add(node);
        }
    }

    public int dist(int si, int ei) {
        if (ei <= si)
            return 0;
        return sList.get(ei).x2 - sList.get(si).x1;
    }

    /**
     * remove Nodes have been merged, and clear fragments at the same time
     * @param window
     * @param lowerWidth the thresh whether it should be regarded as fragments
     * @return
     */
    public SplitList out(int window, int lowerWidth) {
        SplitList rstList = new SplitList();
        Node cur, next, prev = null;
        cur = sList.get(0);
        next = sList.get(1);
        int i;
        for (i = 1; i < sList.size() - 1; ) {
            prev = cur;
            cur = next;
            next = sList.get(i + 1);
            if (cur.x2 - prev.x1 > window && next.x2 - cur.x1 > window) {
                sList.remove(i);
                if (lowerWidth < cur.width())
                    rstList.sList.add(cur);
                continue;
            }
            i++;
        }

        if (cur.x2 - prev.x1 > window && next.x2 - cur.x1 > window) {
            // second Node to last
            sList.remove(i);
            if (lowerWidth < next.width())
                rstList.sList.add(next);
        }
        rstList.standardWidth = this.standardWidth;
        return rstList;
    }

    public List<Node> toNodeList() {
        return this.sList;
    }

    public List<Integer> toSimpleList() {
        List<Integer> rstList = new ArrayList<>();
        for (int i = 0; i < sList.size(); i++) {
            Node node = sList.get(i);
            rstList.add(node.x1);
            rstList.add(node.x2);
        }
        return rstList;
    }
}
字符分割完成!!

接下来来看看分割效果如何~


原图.png
分割字符.png
public class CardOCR {
    ....
    
    public static void main(String []args) {
        String fileName= "/Users/xxx.jpg";
        Mat gray = CVGrayTransfer.grayTransferBeforeScale(fileName);
        Producer producer = new Producer(gray);
        // 定位卡号矩形区域
        Rect mainRect = producer.findMainRect();
        // 设置矩形区域
        producer.setRectOfDigitRow(mainRect);
        List<Mat> normalizedImg = null;
            try {
                producer.digitSeparate();
                Mat dst = producer.getMatListOfDigit().get(0).clone();
                if (producer.getFontType()==CardFonts.FontType.LIGHT_FONT){
                    normalizedImg = resizeDataSetImg(
                                            producer.getMatListOfDigit());

                    // 分割后的字符竖直拼接成一张图
                    Core.vconcat(normalizedImg, dst);
                }
                // imshow("digits", dst);
            } catch (Exception e) {
                e.printStackTrace();
            }
    }

    /**
      大小归一化
    */
    public static List<Mat> resizeDataSetImg(List<Mat> set) {
        List<Mat> rstList = new ArrayList<>();
        for (Mat m : set) {
            rstList.add(CVGrayTransfer.resizeMat(m, standardWidth, 
                                (int)(standardWidth * aspectRation)));
        }

        return rstList;
    }

}

可能有小伙伴注意到没有给出如何判断字体类型的方法,我们来接着往下看吧!《Java + OpenCV 实现银行卡号识别 (3)》

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

推荐阅读更多精彩内容

  • 大体思想银行卡号码的识别可简单分为目标检测及字符识别两个部分。CardOCR_00@2x.png 现在网络上能找的...
    宏颜获水阅读 1,835评论 1 4
  • 库是程序代码的集合,是共享程序代码的一种方式 根据源代码的公开情况,分为开源库和闭源库—>编译后的二进制文件 闭源...
    tushizhan阅读 229评论 0 0
  • 清早,抵达广州。 这是一座我并不陌生的城市。爸爸妈妈年轻时奋斗的地方,我度过无忧无虑小学生活的地方。 可,这么多年...
    叶77的小屋阅读 308评论 1 1
  • 天上的星星 嵌在遥远的夜空 地上的星星 散落在荡漾的河里 城市,与天对情 城市,与地细语 城市在初春时春潮暗涌 云...
    skw_5543阅读 117评论 0 0
  • zx是我现在所在的公司,今天是我入职的第15天了。 早上刚看oa的时候,发现了公司的征文主题,就叫我与zx。因为正...
    狒狒爸爸阅读 140评论 0 0