1. 切边

源图像:


需求:扫描仪扫描到的法律文件,需要切边,去掉边缘空白,这样看上去才真实,人工操作成本与时间花费高,希望程序自动实现,高效、准确。 实现思路:边缘检测 + 轮廓发现或直线检测最大外接矩形。

例子代码:


#include


效果图



总结:先利用 Canny 算子检测图像的轮廓,再利用 findContours 发现轮廓,因为这时候会得到很多轮廓,而我们只需要找到最大的轮廓,这时候可以根据源图像的情况设置过滤的条件(如本例中宽高设为不小于0.75)。找到这个矩形后,再把它画出来即可。

2. 直线检测

源图像:



需求:寻找英语试卷填空题的下划线,这个对后期的切图与自动识别都比较重要。 实现思路:通过图像形态学操作来寻找直线,霍夫获取位置信息与显示

例子代码:


#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
Mat srcImg, roiImg;
void detectLine();
void test()
{
    srcImg = imread("test2.jpg",IMREAD_GRAYSCALE);
    if (srcImg.empty())
    {
        cout << "could not load image...n" << endl;
    }
    namedWindow("Original image", CV_WINDOW_NORMAL);  //CV_WINDOW_NORMAL 鼠标控制显示窗口的大小
    imshow("Original image", srcImg);

    //因为是截图,所以要去掉白边,用ROI图像的方法
    Rect roi = Rect(10, 10, srcImg.cols - 15, srcImg.rows - 15);  //切掉白边
    roiImg = srcImg(roi);
    namedWindow("Roi image", CV_WINDOW_NORMAL);
    imshow("Roi image", roiImg);

    detectLine();
}

void detectLine()
{
Mat binaryImg, morhpImg;
    threshold(roiImg, binaryImg, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);  //二值化
    namedWindow("Binary Result", CV_WINDOW_NORMAL);
    imshow("Binary Result", binaryImg);

    //形态学操作
    Mat kernel = getStructuringElement(MORPH_RECT, Size(60, 1), Point(-1, -1));  
    morphologyEx(binaryImg, morhpImg, MORPH_OPEN, kernel, Point(-1, -1));
    namedWindow("Morphology Result", CV_WINDOW_NORMAL);
    imshow("Morphology Result", morhpImg);

    //膨胀,使得直线更加明显
    kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    dilate(morhpImg, morhpImg, kernel);
    namedWindow("Dilate Result", CV_WINDOW_NORMAL);
    imshow("Dilate Result", morhpImg);

    //霍夫变换标定直线
    vector<Vec4i> lines;   //每条线有两个点,把四个点的坐标存储起来
    HoughLinesP(morhpImg, lines, 1, CV_PI / 180.0, 30, 20.0, 0);  // 1 为极坐标方向上的步长
    Mat resultImg = roiImg.clone();   //输出结果
    cvtColor(resultImg, resultImg, COLOR_GRAY2BGR);
    for (int i = 0; i < lines.size(); i++)
    {
        Vec4i ln = lines[i];  //取出直线
        line(resultImg, Point(ln[0], ln[1]), Point(ln[2], ln[3]), Scalar(0, 0, 255), 2, 8, 0);  //Point(ln[0], ln[1]), Point(ln[2], ln[3])两个坐标
    }
    namedWindow("Hough Lines-Final Result", CV_WINDOW_NORMAL);
    imshow("Hough Lines-Final Result", resultImg);
}
int main()
{
    test();
    waitKey(0);
    return 0;
}


效果图



总结:因为源图像是截图,所以最外围有白边,通常这种情况下可以利用 ROI 图像方法去掉不需要的部分,再通过二值化和形态学操作来寻找直线,最后利用霍夫概率变换获取直线的准确位置。

3. 对象提取

源图像:



需求:对图像中对象(圆形)进行提取,获取这样的对象,去掉其它干扰和非目标对象。并获取圆形的面积和周长。 实现思路:二值分割 + 形态学 + 横纵比计算。

步骤:

第一步:发现轮廓 例子代码:


#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
//第一步
Mat srcImg, binaryImg, dstImg;
void test()
{
    srcImg = imread("case3.png", IMREAD_GRAYSCALE);
    if (srcImg.empty())
    {
        cout << "could not load image...n" << endl;
    }
    namedWindow("Original image", CV_WINDOW_NORMAL);
    imshow("Original image", srcImg);

    //二值化
    threshold(srcImg, binaryImg, 0, 255, THRESH_BINARY | THRESH_OTSU);
    namedWindow("Binary Result", CV_WINDOW_NORMAL);
    imshow("Binary Result", binaryImg);

    //形态学操作,开操作,去掉小的对象,闭操作,连接里面的洞(开闭操作要先获得结构元素)
    Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));  //Point(-1, -1)是中心点,这里是 2 x 2位置
    morphologyEx(binaryImg, dstImg, MORPH_CLOSE, kernel, Point(-1, -1));
    namedWindow("Close Result", CV_WINDOW_NORMAL);
    imshow("Close Result", dstImg);

    kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));  
    morphologyEx(dstImg, dstImg, MORPH_OPEN, kernel, Point(-1, -1));
    namedWindow("Open Result", CV_WINDOW_NORMAL);
    imshow("Open Result", dstImg);

    //轮廓发现
    vector<vector<Point>> contours;  //存储轮廓
    vector<Vec4i> hireachy;
    findContours(dstImg, contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
    Mat resultImg = Mat::zeros(srcImg.size(), CV_8SC3);
    for (int i = 0; i < contours.size(); i++)
    {
        drawContours(resultImg, contours, i, Scalar(0, 0, 255), 2, 8, Mat(), 0, Point());  //画出来
    }
    namedWindow("Final Result", CV_WINDOW_NORMAL);
    imshow("Final Result", resultImg);
}
int main()
{
    test();
    waitKey(0);
    return 0;
}


效果图



第二步:过滤并计算面积和周长 例子代码:


#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
//第二步 通过面积过滤,通过纵横比的测量,圆形的纵横比应该在1:1左右,如果不是1:1,就把它过滤掉
Mat srcImg, binaryImg, dstImg;
void test()
{
    srcImg = imread("case3.png", IMREAD_GRAYSCALE);
    if (srcImg.empty())
    {
        cout << "could not load image...n" << endl;
    }
    namedWindow("Original image", CV_WINDOW_NORMAL);  //CV_WINDOW_NORMAL 使得鼠标可以控制显示窗口的大小
    imshow("Original image", srcImg);

    //二值化
    threshold(srcImg, binaryImg, 0, 255, THRESH_BINARY | THRESH_OTSU);
    namedWindow("Binary Result", CV_WINDOW_NORMAL);
    imshow("Binary Result", binaryImg);

    //形态学操作,开操作,去掉小的对象,闭操作,连接里面的洞(开闭操作要先获得结构元素)
    Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));  //Point(-1, -1)是中心点,这里是 2 x 2位置
    morphologyEx(binaryImg, dstImg, MORPH_CLOSE, kernel, Point(-1, -1));
    namedWindow("Close Result", CV_WINDOW_NORMAL);
    imshow("Close Result", dstImg);

    kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    morphologyEx(dstImg, dstImg, MORPH_OPEN, kernel, Point(-1, -1));
    namedWindow("Open Result", CV_WINDOW_NORMAL);
    imshow("Open Result", dstImg);

    //轮廓发现
    vector<vector<Point>> contours;  //存储轮廓
    vector<Vec4i> hireachy;
    findContours(dstImg, contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());

    //通过面积过滤,通过纵横比的测量,圆形的纵横比应该在1:1左右,如果不是1:1,就把它过滤掉
    Mat resultImg = Mat::zeros(srcImg.size(), CV_8SC3);
    Point cc;
    for (int i = 0; i < contours.size(); i++)
    {
        //面积过滤
        double area = contourArea(contours[i]);  //循环获取指定的面积
        if (area < 100)  //通过循环过滤掉小于100的面积
            continue;
        //横纵比过滤
        Rect rect = boundingRect(contours[i]);
        float ratio = float(rect.width) / float(rect.height);
        if (ratio < 1.1 && ratio> 0.9)  //把满足条件的保留画出来
        {
            drawContours(resultImg, contours, i, Scalar(0, 0, 255), -1, 8, Mat(), 0, Point());  //画出来 第五个参数改为 -1 ,使得整个圆形填充
            cout << "circle area: " << area << endl;  //面积和周长打印出来(像素度量)
            cout << "circle length: " << arcLength(contours[i],true) << endl;

            //找中心点
            int x = rect.x + rect.width / 2;
            int y = rect.y + rect.height / 2;
            cc = Point(x, y);
            circle(resultImg, cc, 2, Scalar(0, 0, 255), 2, 8, 0);  //画出中心点
        }
    }
    namedWindow("Final Result", CV_WINDOW_NORMAL);
    imshow("Final Result", resultImg);

    //在原图上定位显示中心点
    Mat circleImg = srcImg.clone();
    cvtColor(circleImg, circleImg, COLOR_GRAY2BGR); 
    circle(circleImg, cc, 2, Scalar(0, 0, 255), 2, 8, 0);

    namedWindow("Center Point", CV_WINDOW_NORMAL);
    imshow("Center Point", circleImg);
}

int main()
{
    test();
    waitKey(0);
    return 0;
}


效果图



总结:首先把图像二值化,通过闭操作把中间小的洞连接上,再通过开操作把周围小的点去掉,再通过轮廓发现找到对象的轮廓。再通过面积、纵横比过滤点其它不需要的对象,圆形的纵横比应该在1:1左右,如果不是1:1,就把它过滤掉。然后再计算面积和周长。