Opencv - How to get number of vertical lines present in image (count of lines)

Viewed 489

Firstly I integrate OpenCV framework to XCode and All the OpenCV code is on ObjectiveC and I am using in Swift Using bridging header. I am new to OpenCV Framework and trying to achieve count of vertical lines from the image.

Here is my code: First I am converting the image to GrayScale

 + (UIImage *)convertToGrayscale:(UIImage *)image {
    cv::Mat mat;
    UIImageToMat(image, mat);
    cv::Mat gray;
    cv::cvtColor(mat, gray, CV_RGB2GRAY);
    UIImage *grayscale = MatToUIImage(gray);
    return grayscale;
}

Then, I am detecting edges so I can find the line of gray color

+ (UIImage *)detectEdgesInRGBImage:(UIImage *)image {
    cv::Mat mat;
    UIImageToMat(image, mat);
    
    //Prepare the image for findContours
    cv::threshold(mat, mat, 128, 255, CV_THRESH_BINARY);

    //Find the contours. Use the contourOutput Mat so the original image doesn't get overwritten
    std::vector<std::vector<cv::Point> > contours;
    cv::Mat contourOutput = mat.clone();
    cv::findContours( contourOutput, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE );

    NSLog(@"Count =>%lu", contours.size());
    
    //For Blue
    /*cv::GaussianBlur(mat, gray, cv::Size(11, 11), 0); */
    
    UIImage *grayscale = MatToUIImage(mat);
    return grayscale;
}

This both Function is written on Objective C

Here, I am calling both function Swift

override func viewDidLoad() {
        super.viewDidLoad()
        let img = UIImage(named: "imagenamed")
        let img1 = Wrapper.convert(toGrayscale: img)
        self.capturedImageView.image = Wrapper.detectEdges(inRGBImage: img1)
    }

I was doing this for some days and finding some useful documents(Reference Link)

OpenCV - how to count objects in photo?

How to count number of lines (Hough Trasnform) in OpenCV

OPENCV Documents

https://docs.opencv.org/2.4/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html?#findcontours

Basically, I understand the first we need to convert this image to black and white, and then using cvtColor, threshold and findContours we can find the colors or lines.

I am attaching the image that vertical Lines I want to get.

Original Image

Original Image

Output Image that I am getting

Output Image that I am getting

I got number of lines count =>10 I am not able to get accurate count here.

Please guide me on this. Thank You!

2 Answers

Since you want to detect the number of the vertical lines, there is a very simple approach I can suggest for you. You already got a clear output and I used this output in my code. Here are the steps before the code:

  1. Preprocess the input image to get the lines clearly
  2. Check each row and check until get a pixel whose value is higher than 100(threshold value I chose)
  3. Then increase the line counter for that row
  4. Continue on that line until get a pixel whose value is lower than 100
  5. Restart from step 3 and finish the image for each row
  6. At the end, check the most repeated element in the array which you assigned line numbers for each row. This number will be the number of vertical lines.

Note: If the steps are difficult to understand, think like this way:

" I am checking the first row, I found a pixel which is higher than 100, now this is a line edge starting, increase the counter for this row. Search on this row until get a pixel smaller than 100, and then research a pixel bigger than 100. when row is finished, assign the line number for this row to a big array. Do this for all image. At the end, since some lines looks like two lines at the top and also some noises can occur, you should take the most repeated element in the big array as the number of lines."

Here is the code part in C++:

#include <vector>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>


int main()
{
    cv::Mat img = cv::imread("/ur/img/dir/img.jpg",cv::IMREAD_GRAYSCALE);

    std::vector<int> numberOfVerticalLinesForEachRow;


    cv::Rect r(0,0,img.cols-10,200);

    img = img(r);

    bool blackCheck = 1;

    for(int i=0; i<img.rows; i++)
    {
        int numberOfLines = 0;

        for(int j=0; j<img.cols; j++)
        {
            if((int)img.at<uchar>(cv::Point(j,i))>100 && blackCheck)
            {
                numberOfLines++;
                blackCheck = 0;
            }
            if((int)img.at<uchar>(cv::Point(j,i))<100)
                blackCheck = 1;
        }

        numberOfVerticalLinesForEachRow.push_back(numberOfLines);
    }

    // In this part you need a simple algorithm to check the most repeated element
    for(int k:numberOfVerticalLinesForEachRow)
        std::cout<<k<<std::endl;





    cv::namedWindow("WinWin",0);

    cv::imshow("WinWin",img);

    cv::waitKey(0);


}

Here's another possible approach. It relies mainly on the cv::thinning function from the extended image processing module to reduce the lines at a width of 1 pixel. We can crop a ROI from this image and count the number of transitions from 255 (white) to 0 (black). These are the steps:

  1. Threshold the image using Otsu's method
  2. Apply some morphology to clean up the binary image
  3. Get the skeleton of the image
  4. Crop a ROI from the center of the image
  5. Count the number of jumps from 255 to 0

This is the code, be sure to include the extended image processing module (ximgproc) and also link it before compiling it:

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/ximgproc.hpp> // The extended image processing module

// Read Image:
std::string imagePath = "D://opencvImages//";
cv::Mat inputImage = cv::imread( imagePath+"IN2Xh.png" );

// Convert BGR to Grayscale:
cv::cvtColor( inputImage, inputImage, cv::COLOR_BGR2GRAY );

// Get binary image via Otsu:
cv::threshold( inputImage, inputImage, 0, 255, cv::THRESH_OTSU );

The above snippet produces the following image:

Note that there's a little bit of noise due to the thresholding, let's try to remove those isolated blobs of white pixels by applying some morphology. Maybe an opening, which is an erosion followed by dilation. The structuring elements and iterations, though, are not the same, and these where found by experimentation. I wanted to remove the majority of the isolated blobs without modifying too much the original image:

// Apply Morphology. Erosion + Dilation:
// Set rectangular structuring element of size 3 x 3:
cv::Mat SE = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(3, 3) );
// Set the iterations:
int morphoIterations = 1;
cv::morphologyEx( inputImage, inputImage, cv::MORPH_ERODE, SE, cv::Point(-1,-1), morphoIterations);

// Set rectangular structuring element of size 5 x 5:
SE = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(5, 5) );
// Set the iterations:
morphoIterations = 2;
cv::morphologyEx( inputImage, inputImage, cv::MORPH_DILATE, SE, cv::Point(-1,-1), morphoIterations);

This combination of structuring elements and iterations yield the following filtered image:

Its looking alright. Now comes the main idea of the algorithm. If we compute the skeleton of this image, we would "normalize" all the lines to a width of 1 pixel, which is very handy, because we could reduce the image to a 1 x 1 (row) matrix and count the number of jumps. Since the lines are "normalized" we could get rid of possible overlaps between lines. Now, skeletonized images sometimes produce artifacts near the borders of the image. These artifacts resemble thickened anchors at the first and last row of the image. To prevent these artifacts we can extend borders prior to computing the skeleton:

// Extend borders to avoid skeleton artifacts, extend 5 pixels in all directions:
cv::copyMakeBorder( inputImage, inputImage, 5, 5, 5, 5, cv::BORDER_CONSTANT, 0 );

// Get the skeleton:
cv::Mat imageSkelton;
cv::ximgproc::thinning( inputImage, imageSkelton );

This is the skeleton obtained:

Nice. Before we count jumps, though, we must observe that the lines are skewed. If we reduce this image directly to a one row, some overlapping could indeed happen between to lines that are too skewed. To prevent this, I crop a middle section of the skeleton image and count transitions there. Let's crop the image:

// Crop middle ROI:
cv::Rect linesRoi;
linesRoi.x = 0;
linesRoi.y = 0.5 * imageSkelton.rows;
linesRoi.width = imageSkelton.cols;
linesRoi.height = 1;

cv::Mat imageROI = imageSkelton( linesRoi );

This would be the new ROI, which is just the middle row of the skeleton image:

Let me prepare a BGR copy of this just to draw some results:

// BGR version of the Grayscale ROI:
cv::Mat colorROI;
cv::cvtColor( imageROI, colorROI, cv::COLOR_GRAY2BGR );

Ok, let's loop through the image and count the transitions between 255 and 0. That happens when we look at the value of the current pixel and compare it with the value obtained an iteration earlier. The current pixel must be 0 and the past pixel 255. There's more than a way to loop through a cv::Mat in C++. I prefer to use cv::MatIterator_s and pointer arithmetic:

// Set the loop variables:
cv::MatIterator_<cv::Vec3b> it, end;
uchar pastPixel = 0;
int jumpsCounter = 0;
int i = 0; 

// Loop thru image ROI and count 255-0 jumps:
for (it = imageROI.begin<cv::Vec3b>(), end = imageROI.end<cv::Vec3b>(); it != end; ++it) {

    // Get current pixel
    uchar &currentPixel = (*it)[0];
    // Compare it with past pixel:
    if ( (currentPixel == 0) && (pastPixel == 255) ){
        // We have a jump:
        jumpsCounter++;
        // Draw the point on the BGR version of the image:
        cv::line( colorROI, cv::Point(i, 0), cv::Point(i, 0), cv::Scalar(0, 0, 255), 1 );
    }

    // current pixel is now past pixel:
    pastPixel = currentPixel;
    i++;

}

// Show image and print number of jumps found:
cv::namedWindow( "Jumps Found", CV_WINDOW_NORMAL );
cv::imshow( "Jumps Found", colorROI );
cv::waitKey( 0 );

std::cout<<"Jumps Found: "<<jumpsCounter<<std::endl;

The points where the jumps were found are drawn in red, and the number of total jumps printed is:

Jumps Found: 9
Related