第 7 章 提取直线、轮廓和区域


本章包括以下内容:

  • 用Canny 算子检测图像轮廓;
  • 用霍夫变换检测直线;
  • 点集的直线拟合;
  • 提取连续区域;
  • 计算区域的形状描述子。


7.2 用Canny 算子检测图像轮廓

简单的二值边缘分布图有两个主要缺点:第一,检测到的边缘过厚,这加大了识别物体边界的难度;第二,通常不可能找到既低到足以检测到图像中所有重要边缘,又高到足以避免产生太多无关紧要边缘的阈值。这是一个难以权衡的问题,Canny 算法试 图解决这个问题。

Canny 算法可通过OpenCV 的cv::Canny 函数实现。使用这个算法时,需要指定两个阈值。调用函数的方法如下所示:

    // 应用Canny 算法
    cv::Mat contours;
    cv::Canny(image, // 灰度图像
        contours, // 输出轮廓
        125, // 低阈值
        350); // 高阈值

先来看这幅图像。

road.jpg

在这幅图像上应用Canny 算法,得到如下结果。

result.jpg

注意,因为正常的结果是用非零像素表示轮廓的,所以这里在显示轮廓时做了反转处理。上面显示的图像只是像素值为255 的轮廓。

Canny 算子通常基于第6 章介绍的Sobel 算子,虽然也可使用其他的梯度算子。它的核心理念是用两个不同的阈值来判断哪个点属于轮廓,一个是低阈值,一个是高阈值。

选择低阈值时,要保证它能包含所有属于重要图像轮廓的边缘像素。例如,将前面例子中指定的低阈值应用到Sobel 算子返回的图像上,可得到如下边缘分布图。

result.jpg

可以看到,道路的边缘非常清晰。但因为这里使用了一个宽松的阈值,所以很多并不需要的边缘也被检测出来了。而第二个阈值的作用就是界定重要轮廓的边缘,排除掉异常的边缘。例如,在Sobel 边缘分布图上应用上例中的高阈值后,将得到如下结果。

result.jpg

现在得到的图像中有些边缘是断裂的,但是这些可见的边缘肯定属于本场景中的重要轮廓。Canny 算法将结合这两种边缘分布图,生成最优的轮廓分布图。具体做法是在低阈值边缘分布图上只保留具有连续路径的边缘点,同时把那些边缘点连接到属于高阈值边缘分布图的边缘上。这样一来,高阈值分布图上的所有边缘点都被保留下来,而低阈值分布图上边缘点的孤立链全部被移除。这是一种很好的折中方案,只要指定适当的阈值,就能获得高质量的轮廓。这种基于两个阈值获得二值分布图的策略被称为滞后阈值化,可用于任何需要用阈值化获得二值分布图的场景。但是它的计算复杂度比较高。

另外,Canny 算法用了一个额外的策略来优化边缘分布图的质量。在进行滞后阈值化之前,如果梯度幅值不是梯度方向上的最大值,那么对应的边缘点都会被移除(前面讲过,梯度的方向总是与边缘垂直的)。因此,这个方向上梯度的局部最大值对应着轮廓最大强度的位置。这是一个细化轮廓的运算,它创建的轮廓宽度只有一个像素。这也解释了为什么Canny 轮廓分布图的边缘比较薄。


7.3 用霍夫变换检测直线

霍夫变换(Hough transform)是一种常用于检测直线特征的经典算法。该算法起初用于检测图像中的直线,后来经过扩展,也能检测其他简单的图像结构。

针对用于检测直线的霍夫变换,OpenCV 提供了两种实现方法。

基础版是cv::HoughLines。它输入的是一个二值分布图,其中包含一批像素点(用非零像素表示),一些对齐的点构成了直线。它通常是一个已经生成的边缘分布图,例如Canny 算子生成的分布图。输出的是一个cv::Vec2f 类型元素组成的向量,每个元素是一对浮点数,表示检测到的直线的参数,即(ρ, θ)。

下面是使用这个函数的例子,首先用Canny 算子获得图像轮廓,然后用霍夫变换检测直线:

    // 应用Canny 算法
    cv::Mat contours;
    cv::Canny(image, contours, 125, 350);

    // 用霍夫变换检测直线
    std::vector<cv::Vec2f> lines;
    cv::HoughLines(contours, lines, 1,
        PI / 180, // 步长
        60); // 最小投票数

第3 个和第4 个参数表示搜索直线时用的步长。在本例中,半径步长为1,表示函数将搜索所有可能的半径;角度步长为π/180,表示函数将搜索所有可能的角度。

为了让检测结果可视化,我们在原始图像上绘制这些直线。有一点需要强调,这个算法检测的是图像中的直线而不是线段,它不会给出直线的端点。因此,我们绘制的直线将穿透整幅图像。通过遍历直线向量画出所有直线,代码如下所示:

    cv::Mat result(contours.rows, contours.cols, CV_8U, cv::Scalar(255));
    image.copyTo(result);

    std::vector<cv::Vec2f>::const_iterator it = lines.begin();
    while (it != lines.end()) {
        float rho = (*it)[0]; // 第一个元素是距离rho
        float theta = (*it)[1]; // 第二个元素是角度theta
        if (theta < PI / 4. || theta > 3. * PI / 4.) { // 垂直线(大致)
        // 直线与第一行的交叉点
            cv::Point pt1(rho / cos(theta), 0);
            // 直线与最后一行的交叉点
            cv::Point pt2((rho - result.rows * sin(theta)) /
                cos(theta), result.rows);
            // 画白色的线
            cv::line(image, pt1, pt2, cv::Scalar(255), 1);
        }
        else { // 水平线(大致)
     // 直线与第一列的交叉点
            cv::Point pt1(0, rho / sin(theta));
            // 直线与最后一列的交叉点
            cv::Point pt2(result.cols,
                (rho - result.cols * cos(theta)) / sin(theta));
            // 画白色的线
            cv::line(image, pt1, pt2, cv::Scalar(255), 1);
        }
        ++it;
    }

得到的结果如下所示。

result.jpg

可以看出,霍夫变换只是寻找图像中边缘像素的对齐区域。因为有些像素只是碰巧排成了直线,所以霍夫变换可能产生错误的检测结果。也可能因为多条参数相近的直线穿过了同一个像素对齐区域,而导致检测出重复的结果。

为解决上述问题并检测到线段(即包含端点的直线),人们提出了霍夫变换的改进版。这就是概率霍夫变换,在OpenCV 中通过cv::HoughLinesP 函数实现。我们用它创建LineFinder类,封装函数的参数:

class LineFinder {
private:
    // 原始图像
    cv::Mat img;
    // 包含被检测直线的端点的向量
    std::vector<cv::Vec4i> lines;
    // 累加器分辨率参数
    double deltaRho;
    double deltaTheta;
    // 确认直线之前必须收到的最小投票数
    int minVote;
    // 直线的最小长度
    double minLength;
    // 直线上允许的最大空隙
    double maxGap;
public:
    // 默认累加器分辨率是1 像素,1 度
    // 没有空隙,没有最小长度
    LineFinder() : deltaRho(1), deltaTheta(PI / 180),
        minVote(10), minLength(0.), maxGap(0.) {}

看一下对应的设置方法:

// 设置累加器的分辨率
    void setAccResolution(double dRho, double dTheta) {
        deltaRho = dRho;
        deltaTheta = dTheta;
    }
    // 设置最小投票数
    void setMinVote(int minv) {
        minVote = minv;
    }
    // 设置直线长度和空隙
    void setLineLengthAndGap(double length, double gap) {
        minLength = length;
        maxGap = gap;
    }
    用上述方法,检测霍夫线段的代码如下所示:
        // 应用概率霍夫变换
        std::vector<cv::Vec4i> findLines(cv::Mat& binary) {
        lines.clear();
        cv::HoughLinesP(binary, lines,
            deltaRho, deltaTheta, minVote,
            minLength, maxGap);
        return lines;
    }

这个方法返回cv::Vec4i 类型的向量,包含每条被检测线段的开始端点和结束端点的坐标。我们可以用下面的方法在图像上绘制检测到的线段:

    // 在图像上绘制检测到的直线
    void drawDetectedLines(cv::Mat& image, cv::Scalar color=cv::Scalar(255, 255, 255)) {
        // 画直线
        std::vector<cv::Vec4i>::const_iterator it2 = lines.begin();
        while (it2 != lines.end()) {
            cv::Point pt1((*it2)[0], (*it2)[1]);
            cv::Point pt2((*it2)[2], (*it2)[3]);
            cv::line(image, pt1, pt2, color);
            ++it2;
        }
    }

输入图像不变,可以用下面的次序检测直线:

    // 创建LineFinder 类的实例
    LineFinder finder;
    // 设置概率霍夫变换的参数
    finder.setLineLengthAndGap(100, 20);
    finder.setMinVote(60);
    // 检测直线并画线
    std::vector<cv::Vec4i> lines = finder.findLines(contours);
    finder.drawDetectedLines(image);

上面的代码得到如下结果。

result.jpg

检测圆

霍夫变换也能用来检测其他几何物体。事实上,任何可以用一个参数方程来表示的物体,都很适合用霍夫变换来检测。还有一种泛化霍夫变换,可以检测任何形状的物体。

cv::HoughCircles 函数将Canny 检测与霍夫变换结合,它的调用方法是:

cv::GaussianBlur(image, image, cv::Size(5, 5), 1.5);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, cv::HOUGH_GRADIENT,
    2, // 累加器分辨率(图像尺寸/2)
    50, // 两个圆之间的最小距离
    200, // Canny 算子的高阈值
    100, // 最少投票数
    25,
    100); // 最小和最大半径

有一点需要反复提醒:在调用cv::HoughCircles 函数之前,要对图像进行平滑化,以减少图像中可能导致误判的噪声。检测的结果存放在cv::Vec3f 实例的向量中。前面两个数值是圆心坐标,第三个数值是半径。

得到存放圆的向量后,就可以在图像上画出这些圆。方法是迭代遍历该向量,并调用cv::circle 函数,传入获得的参数:


std::vector<cv::Vec3f>::const_iterator itc = circles.begin();
while (itc != circles.end()) {
    cv::circle(image,
        cv::Point((*itc)[0], (*itc)[1]), // 圆心
        (*itc)[2], // 半径
        cv::Scalar(255), // 颜色
        2); // 厚度
    ++itc;
}

使用上述方法和参数在测试图像上执行,得到如下结果。

result.jpg

霍夫变换的目的是在二值图像中找出全部直线,并且这些直线必须穿过足够多的像素点。它的处理方法是,检查输入的二值分布图中每个独立的像素点,识别出穿过该像素点的所有可能直线。如果同一条直线穿过很多像素点,就说明这条直线明显到足以被认定。


7.4 点集的直线拟合

在某些应用程序中,光是检测出图像中的直线还不够,还需要精确地估计直线的位置和方向。本节将介绍如何拟合出最适合指定点集的直线。

首先需要识别出图像中靠近直线的点。使用一条上节检测到的直线。把cv::HoughLinesP检测到的直线存放在std::vector<cv::Vec4i>类型的变量lines 中。为了提取出靠近这条直线(我们叫它第一条直线)的点集,可以继续以下步骤:在黑色图像上画一条白色直线,并且穿过用于检测直线的Canny 轮廓图。这可以用这些语句实现:

    int n = 0; // 选用直线0
    // 黑色图像
    cv::Mat oneline(contours.size(), CV_8U, cv::Scalar(0));
    // 白色直线
    cv::line(oneline, cv::Point(lines[n][0], lines[n][1]),
        cv::Point(lines[n][2], lines[n][3]), cv::Scalar(255), 3); // 直线宽度
        // 轮廓与白色直线进行“与”运算
    cv::bitwise_and(contours, oneline, oneline);

结果是一个包含了与指定直线相关的点的图像。为了引入公差,我们画了具有一定宽度(这里是3)的直线,因此位于指定邻域内的点都能被接受。

得到的图像如下所示(为了提升显示效果,对其做了反转)。

result.jpg

然后可以把这些集合内点的坐标插入到cv::Point 对象的std::vector 类型中(也可以使用浮点数坐标,即cv::Point2f),代码如下所示:

    std::vector<cv::Point> points;
    // 迭代遍历像素,得到所有点的位置
    for (int y = 0; y < oneline.rows; y++) {
        // 行y
        uchar* rowPtr = oneline.ptr<uchar>(y);
        for (int x = 0; x < oneline.cols; x++) {
            // 列x
            // 如果在轮廓上
            if (rowPtr[x]) {
                points.push_back(cv::Point(x, y));
            }
        }
    }

得到点集后,利用这些点集拟合出直线。利用OpenCV 的函数cv::fitLine 可以很轻松地得到最优的拟合直线:

    cv::Vec4f line;
    cv::fitLine(points, line,
        cv::DIST_L2, // 距离类型
        0, // L2 距离不用这个参数
        0.01, 0.01); // 精度

上述代码把直线方程式作为参数,形式是一个单位方向向量(cvVec4f 的前两个数值)和直线上一个点的坐标(cvVec4f 的后两个数值)。最后两个参数是所需的直线精度。

直线方程式通常用于某些属性的计算(例如需要精确参数的校准)。为了演示它的用法,也为了验证计算的直线是否正确,我们在图像上模拟一条直线。这里只是随意画了一条长度为100像素、宽度为2 像素的黑色线段(为了便于观察):

    int x0 = line[2]; // 直线上的一个点
    int y0 = line[3];
    int x1 = x0 + 100 * line[0]; // 加上长度为100 的向量
    int y1 = y0 + 100 * line[1]; //(用单位向量生成)
    // 绘制这条线
    cv::line(image, cv::Point(x0, y0), cv::Point(x1, y1),
        0.2); // 颜色和宽度

下图显示了与道路边界非常一致的直线。

result.jpg


7.5 提取连续区域

图像通常包含各种物体,图像分析的目的之一就是识别和提取这些物体。在物体检测和识别程序中,第一步通常就是生成二值图像,找到感兴趣物体所处的位置。下一个步骤是从由1 和 0 组成的像素集合中提取出物体。

来看第5 章的水牛二值图像。

result.jpg

执行一次简单的阈值化操作,然后应用形态学滤波器,就能获得这幅图像。本节将介绍如何从这样的图像中提取物体。具体来说,就是提取连续区域,即二值图像中由一批连通的像素构成的形状。

OpenCV 提供了一个简单的函数,可以提取出图像中连续区域的轮廓,这个函数就是cv::findContours:

// 用于存储轮廓的向量
std::vector<std::vector<cv::Point>> contours;
cv::findContours(image,
    contours, // 存储轮廓的向量
    cv::RETR_EXTERNAL, // 检索外部轮廓
    cv::CHAIN_APPROX_NONE); // 每个轮廓的全部像素

显然,函数输入的就是上述二值图像。输出的是一个存储轮廓的向量,每个轮廓用一个cv::Point 类型的向量表示。因此输出参数是一个由std::vector 实例构成的std::vector实例。

此外,函数还指明了两个选项,第一个选项表示只检索外部轮廓,即物体内部的空穴会被忽略;第二个选项指明了轮廓的格式。使用当前的选项,向量将列出轮廓的全部点。如使用cv::CHAIN_APPROX_SIMPLE,则只会列出包含水平、垂直或对角线轮廓的端点。用其他选项可得到逼近轮廓的更复杂的链,对轮廓的表示将更紧凑。

在前面的图像中可检测到9 个连续区域,用contours.szie()查看轮廓的数量。

有一个非常实用的函数可在图像(这里用白色图像)上画出那些区域的轮廓:

// 在白色图像上画黑色轮廓
cv::Mat result(image.size(), CV_8U, cv::Scalar(255));
cv::drawContours(result, contours,
    -1, // 画全部轮廓
    0, // 用黑色画
    2); // 宽度为2

如果这个函数的第三个参数是负数,就画出全部轮廓,否则就可以指定要画的轮廓的序号,得到的结果如下所示。

result.jpg

提取轮廓的算法很简单,它系统地扫描图像,直到找到连续区域。从区域的起点开始,沿着它的轮廓对边界像素做标记。处理完这个轮廓后,就从上个位置继续扫描,直到发现新的区域。

你也可以对识别出的连续区域进行独立的分析。例如,如果事先已经知道感兴趣物体的大小,就可以将部分区域删除。我们采用区域边界的最小值和最大值,具体做法是迭代遍历存放轮廓的向量,并且删除无效的轮廓:

    // 删除太短或太长的轮廓
    int cmin = 50; // 最小轮廓长度
    int cmax = 500; // 最大轮廓长度
    std::vector<std::vector<cv::Point>>::
        iterator itc = contours.begin();
    // 针对所有轮廓
    while (itc != contours.end()) {
        // 验证轮廓大小
        if (itc->size() < cmin || itc->size() > cmax)
            itc = contours.erase(itc);
        else
            ++itc;
    }

因为std::vector 中的删除操作的时间复杂度为O(N),所以这个循环的效率还可以更高。不过这种小型向量的总体开销也不会不大。

这次我们在原始图像上画出剩下的轮廓,结果如下所示。

result.jpg

cv::findContours 函数也能检测二值图像中的所有闭合轮廓,包括区域内部空穴构成的轮廓。实现方法是在调用函数时指定另一个标志:

cv::findContours(image,
    contours, // 存放轮廓的向量
    cv::RETR_LIST, // 检索全部轮廓
    cv::CHAIN_APPROX_NONE); // 全部像素

调用后得到如下轮廓。

result.jpg

注意,背景森林中增加了额外的轮廓。你也可以把这些轮廓分层次组织起来。主区域是父轮廓,它内部的空穴是子轮廓;如果空穴内部还有区域,那它们就是上述子轮廓的子轮廓,以此类推。使用cv::RETR_TREE 标志可得到这个层次结构,代码为:

std::vector<cv::Vec4i> hierarchy;
cv::findContours(image, contours, // 存放轮廓的向量
    hierarchy, // 层次结构
    cv::RETR_TREE, // 树状结构的轮廓
    cv::CHAIN_APPROX_NONE); // 每个轮廓的全部像素

本例中每个轮廓都有一个对应的层次元素,存放次序与轮廓相同。层次元素由四个整数构成,前两个整数是下一个和上一个同级轮廓的序号,后两个整数是第一个子轮廓和父轮廓的序号。如果序号为负,就表示轮廓列表的末端。cv::RETR_CCOMP 标志的作用与之类似,但只允许两个层次。


7.6 计算区域的形状描述子

本节将介绍几种OpenCV 的形状描述子,用于描述连续区域的形状。

OpenCV 中用于形状描述的函数有很多,我们把其中的几个应用到上节提取的区域。在下面的代码段中,我们将计算轮廓的形状描述子(从contours[0]到contours[3]),并在轮廓图像(宽度为1)上画出结果(宽度为2)。

第一个是边界框,用于右下角的区域:

// 测试边界框
cv::Rect r0= cv::boundingRect(contours[0]);
// 画矩形
cv::rectangle(result,r0, 0, 2)

最小覆盖圆的情况也类似,将它用于右上角的区域:

// 测试覆盖圆
float radius;
cv::Point2f center;
cv::minEnclosingCircle(contours[1],center,radius);
// 画圆形
cv::circle(result,center, static_cast<int>(radius), cv::Scalar(0),2);

计算区域轮廓的多边形逼近的代码如下(位于左侧区域):

// 测试多边形逼近
std::vector<cv::Point> poly;
cv::approxPolyDP(contours[2],poly,5,true);
// 画多边形
cv::polylines(result, poly, true, 0, 2);

注意,多边形绘制函数cv::polylines 与其他画图函数很相似。第三个布尔型参数表示该轮廓是否闭合(如果闭合,最后一个点将与第一个点相连)。

凸包是另一种形式的多边形逼近(位于左侧第二个区域)

// 测试凸包
std::vector<cv::Point> hull;
cv::convexHull(contours[3],hull);
// 画多边形
cv::polylines(result, hull, true, 0, 2);

最后,计算轮廓矩是另一种功能强大的描述子(在所有区域内部画出重心):

    // 测试轮廓矩
    // 迭代遍历所有轮廓
    itc = contours.begin();
    while (itc != contours.end()) {
        // 计算所有轮廓矩
        cv::Moments mom = cv::moments(cv::Mat(*itc++));
        // 画重心
        cv::circle(result,
            // 将重心位置转换成整数
            cv::Point(mom.m10 / mom.m00, mom.m01 / mom.m00),
            2, cv::Scalar(0), 2); // 画黑点
    }

结果如下所示。

result.jpg

在表示和定位图像中区域的方法中,边界框可能是最简洁的。它的定义是:能完整包含该形状的最小垂直矩形。比较边界框的高度和宽度,可以获得物体在垂直或水平方向的特征(例如可以通过计算高度与宽度的比例,分辨出一幅图像是汽车还是行人)。

最小覆盖圆通常在只需要区域尺寸和位置的近似值时使用。

如果要更紧凑地表示区域的形状,可采用多边形逼近。在创建时要制定精确度参数,表示形状与对应的简化多边形之间能接受的最大距离。它是cv::approxPolyDP 函数的第四个参数。返回的结果是cv::Point 类型的向量,表示多边形的顶点个数。在画这个多边形时,要迭代遍历整个向量,并在顶点之间画直线,把它们逐个连接起来。

形状的凸包(或凸包络)是包含该形状的最小凸多边形。可以把它看作一条绕在区域周围的橡皮筋。可以看出,在形状轮廓中凹进去的位置,凸包轮廓会与原始轮廓发生偏离。

通常可用凸包缺陷来表示这些位置。OpenCV 中有一个专门用于识别凸包缺陷的函数cv::convexityDefects,它的调用方法如下所示:

std::vector<cv::Vec4i> defects;
cv::convexityDefects(contour, hull, defects);

参数contour 和hull 分别表示原始轮廓和凸包轮廓(两者都用std::vector<cv::Point>的实例表示)。函数输出的是一个向量,它的每个元素由四个整数组成:前两个整数是顶点在轮廓中的索引,用来界定该缺陷;第三个整数表示凹陷内部最远的点;最后的整数表示最远点与凸包之间的距离。

轮廓矩是形状结构分析中常用的数学模型。OpenCV 定义了一个数据结构,封装了形状中计算得到的所有轮廓矩。它是函数cv::moments 的返回值。这些轮廓矩共同表示物体形状的紧凑程度,常用于特征识别。我们只是用该结构获得每个区域的重心,这里用前面三个空间轮廓矩计算得到。

四边形检测

第5 章讲到的MSER 特征是一种高效的工具,可以从图像中提取形状。利用前面用MSER得到的结果,我们来构建一个在图像中监测四边形区域的算法。在当前图像中,该算法可用于检测建筑物的窗户。要获取MSER 的二值图像非常简单:

    cv::Mat components;
    components = cv::imread("mser.bmp", 0);
    components = components == 255;
    cv::morphologyEx(components, components, cv::MORPH_OPEN,
        cv::Mat(), cv::Point(-1, -1), 3);

得到的图像如下所示。

result.jpg

下一步是获取轮廓:

    std::vector<std::vector<cv::Point> > contours;
    // 翻转图像(背景必须是黑色的)
    cv::Mat componentsInv = 255 - components;
    // 得到连续区域的轮廓
    cv::findContours(componentsInv,
        contours, // 轮廓的向量
        cv::RETR_EXTERNAL, // 检索外部轮廓
        cv::CHAIN_APPROX_NONE); 

最后得到全部轮廓,并用多边形粗略地逼近它们:

    // 白色图像
    cv::Mat quadri(components.size(), CV_8U, 255);

    // 针对全部轮廓
    std::vector<std::vector<cv::Point>>::iterator it = contours.begin();
    while (it != contours.end()) {
        std::vector<cv::Point> poly;
        // 用多边形逼近轮廓
        cv::approxPolyDP(*it, poly, 5, true);
        // 是否为四边形?
        if (poly.size() == 4) {
            // 画出来
            cv::polylines(quadri, poly, true, 0, 2);
        }
        ++it;
    }

检测结果如下所示。

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

推荐阅读更多精彩内容

  • 1、阈值分割 1.1 简介 图像阈值化分割是一种传统的最常用的图像分割方法,因其实现简单、计算量小、性能较稳定而成...
    木夜溯阅读 22,527评论 9 15
  • 1、阈值分割 1.1 简介 图像阈值化分割是一种传统的最常用的图像分割方法,因其实现简单、计算量小、性能较稳定而成...
    Lornatang阅读 9,436评论 0 5
  • http://blog.csdn.net/x454045816/article/details/52153250 ...
    G风阅读 6,990评论 0 1
  • 这篇文章总结比较全面:http://blog.csdn.net/timidsmile/article/detail...
    rogerwu1228阅读 1,775评论 0 3
  • 从前,有一位农夫养了一只漂亮的大公鸡。可是每天早晨起床后,大公鸡却不叫鸣,只顾着打扮自己。你看,他系着领带,扎着蝴...
    葆婴皓妈阅读 2,833评论 0 3