-2

This question is an extension to my previous question asking about how to detect a pool table's corners. I have found the outline of a pool table, and I have managed to apply the Hough transform on the outline. The result of this Hough transform is below:

Outline with Hough Transform

Unfortunately, the Hough transform returns multiple lines for a single table edge. I want the Hough transform to return four lines, each corresponding to an edge of the table given any image of a pool table. I don't want to tweak the parameters for the Hough transform method manually (because the outline of the pool table might differ for each image of the pool table). Is there any way to guarantee four lines to be generated by cv2.HoughLines()?

Thanks in advance.

EDIT

Using @fana's comments, I have created a histogram of gradient directions with the code below. I'm still not entirely sure how to obtain four lines from this histogram.

img = cv2.imread("Assets/Setup.jpg")
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
masked_img = cv2.inRange(hsv_img, (50, 40, 40), (70, 255, 255))
gaussian_blur_img = cv2.GaussianBlur(masked_img, (5, 5), 0)
sobel_x = np.asarray([[1, 0, -1], [2, 0, -2], [1, 0, -1]], dtype=np.int8)
sobel_y = np.asarray([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], dtype=np.int8)
gradient_x = cv2.filter2D(gaussian_blur_img, cv2.CV_16S, cv2.flip(sobel_x, -1), borderType=cv2.BORDER_CONSTANT)
gradient_y = cv2.filter2D(gaussian_blur_img, cv2.CV_16S, cv2.flip(sobel_y, -1), borderType=cv2.BORDER_CONSTANT)
edges = cv2.normalize(np.hypot(gradient_x, gradient_y), None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
edge_direction = np.arctan2(gradient_y, gradient_x) * (180 / np.pi)
edge_direction[edge_direction < 0] += 360
np.around(edge_direction, 0, edge_direction)
edge_direction[edge_direction == 360] = 0
edge_direction = edge_direction.astype("uint16")
histogram, bins = np.histogram(edge_direction, 359)
  • No, not that I know about in any direct way. – fmw42 May 07 '22 at 01:48
  • 1
    Why use cv.HoughLines () suddenly? If it can be assumed that the outline is mainly composed of four line segments, consider splitting the pixels on the outline into 4 groups at first. e.g. Consider voting only in the edge(gradient) direction. (It can also be called 1D Hough Transform.) In other words, create a histogram of gradient direction. After split, do line-fitting to them, respectively. – fana May 07 '22 at 02:05
  • 1
    Using cv.HoughLines(), you can not access to voting space, not acquire the information "Who voted to here?". This is very inconvenient in practice. Therefore, if you employ the method "Hough Transform", I recommend that you implement it yourself. – fana May 07 '22 at 02:22
  • @fana Would you mind elaborating on what you mean by "voting only in the edge direction"/"create a histogram of gradient direction"? –  May 07 '22 at 02:40
  • 1
    Pixels belonging to the same line have similar direction (ideally the same). And, the pixel belonging to other lines have different direction. (It looks like that in your image presented in the previous question) Therefore, when you create the Histogram, you will see four local maximums. You can split pixels into 4 groups based on "which pixel voted for which bin". – fana May 07 '22 at 10:00
  • @fana Thanks so much for the prompt response. From these four local maximums, we then split the pixel into groups and perform line fitting, right? –  May 07 '22 at 23:07
  • 1
    Yes, my estimation is that, pixels that voted to a local-max-bin (and bins within a range close enough to it) will be on the same line. 4 such pixel groups will be found, I think. So, doing line-fitting to each group, number of lines you get becomes to 4. – fana May 08 '22 at 02:08
  • 1
    Note that, I don't know if we can get satisfactory accuracy, but at the time of grouping, the parameters of each straight line are already obtained. (Accurate/Robust line-fitting is optional.) – fana May 08 '22 at 02:20
  • 1
    Line is defined with Direction and 1 Position on the line. The group already has those statistical values : the Direction is voted information, and for example center of gravity of pixels can be used for the Position. – fana May 08 '22 at 02:27
  • 1
    (Of course, the mean value can be used for the Direction as well as the Position.) – fana May 08 '22 at 02:33
  • Thanks for the elaborate response! I think I get the overall process now. How should I begin implementing this histogram of gradient direction? I used cv2.Canny() to obtain the outline of the pool table. I know cv2.Canny() does not provide edge direction information, so instead, I will apply Sobel filters Gx and Gy and calculate the magnitude and edge direction images myself. But I'm not exactly sure where to go from there. –  May 08 '22 at 18:08
  • 1
    You can calculate angle from Gx and Gy, ( e.g. with cv.cartToPolar, or arc-tangent for each pixel). Now pixels can vote for direction (angle). – fana May 09 '22 at 01:14
  • I don't know how to make a function that votes based on direction, as we described above, though. Do you know any easy implementations? –  May 09 '22 at 01:34
  • 1
    Simply put, e.g. an array which has 360 integer elements can be used as voting space. At first initialize with all 0. Then, calculate angle(in degree, in this case) for each outline pixel, and vote (increment array element indicated by the calculated angle). Of course, there may be better/convenient implementation, but I recommend to try such a simple implementation and see the voting result, as first step of your trial and error. – fana May 09 '22 at 07:52
  • @fana Thanks so much. I've updated the question to reflect the changes I've made (I've made a histogram of gradient direction), but I'm not sure how I can split this histogram into four (and use the split to find four lines). –  May 10 '22 at 22:09

1 Answers1

0

Using @fana's comments, I have created a histogram of gradient directions with the code below. I'm still not entirely sure how to obtain four lines from this histogram.

I tried a little.

Because I don't know python, following sample code is C++. However, what done are written as comment, so I seems that you will be able to understood.

This sample is including the followings:

  • Extract the outline of the pool table.
  • Create Gradient-Direction Histogram (gradient is estimated with Sobel filter).
  • Find pixel groups based on Histogram peaks.

This sample is not including line-fitting process.

Looking the grouping result, it seems that some pixels will become outlier for line fitting. Therefore, it is better to employ some robust fitting method (e.g. M-estimator, RANSAC), I think.

int main()
{
    //I obtained this image from your previous question.
    //However, I do not used as it is.
    //This image "PoolTable.png" is 25% scale version.
    //(Because your original image was too large for my monitor!)
    cv::Mat SrcImg = cv::imread( "PoolTable.png" ); //Size is 393x524[pixel]
    if( SrcImg.empty() )return 0;

    //Extract Outline Pixels
    std::vector< cv::Point > OutlinePixels;
    {
        //Here, I adjusted a little.
        //  - Change argument value for inRange
        //  - Emplying morphologyEx() additionally.
        cv::Mat HSVImg;
        cv::cvtColor( SrcImg, HSVImg, cv::COLOR_BGR2HSV );
        cv::Mat Mask;
        cv::inRange( HSVImg, cv::Scalar(40,40,40), cv::Scalar(80,255,255), Mask );
        cv::morphologyEx( Mask, Mask, cv::MORPH_OPEN, cv::Mat() );

        //Here, outline is found as the contour which has max area.
        std::vector< std::vector<cv::Point> > contours;
        cv::findContours( Mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE );
        if( contours.empty() )return 0;

        int MaxAreaIndex = 0;
        double MaxArea=0;
        for( int iContour=0; iContour<contours.size(); ++iContour )
        {
            double Area = cv::contourArea( contours[iContour] );
            if( MaxArea < Area ){   MaxArea = Area; MaxAreaIndex = iContour;    }
        }
        OutlinePixels = contours[MaxAreaIndex];
    }

    //Sobel
    cv::Mat Gx,Gy;
    {
        const int KernelSize = 5;
        cv::Mat GraySrc;
        cv::cvtColor( SrcImg, GraySrc, cv::COLOR_BGR2GRAY );
        cv::Sobel( GraySrc, Gx, CV_32F, 1,0, KernelSize );
        cv::Sobel( GraySrc, Gy, CV_32F, 0,1, KernelSize );
    }

    //Voting
    //  Here, each element is the vector of index of point.
    //  (Make it possible to know which pixel voted where.)
    std::vector<int> VotingSpace[360];  //360 Bins
    for( int iPoint=0; iPoint<OutlinePixels.size(); ++iPoint ) //for all outline pixels
    {
        const cv::Point &P = OutlinePixels[iPoint];
        float gx = Gx.at<float>(P);
        float gy = Gy.at<float>(P);
        //(Ignore this pixel if magnitude of gradient is weak.)
        if( gx*gx + gy*gy < 100*100 )continue;
        //Determine the bin to vote based on the angle
        double angle_rad = atan2( gy,gx );
        double angle_deg = angle_rad * 180.0 / CV_PI;
        int BinIndex = cvRound(angle_deg);
        if( BinIndex<0 )BinIndex += 360;
        if( BinIndex>=360 )BinIndex -= 360;
        //Vote
        VotingSpace[ BinIndex ].push_back( iPoint );
    }

    //Find Pixel-Groups Based on Voting Result.
    std::vector< std::vector<cv::Point> > PixelGroups;
    {
        //- Create Blurred Vote count (used for threshold at next process)
        //- Find the bin with the fewest votes (used for start bin of serching at next process)
        unsigned int BlurredVotes[360];
        int MinIndex = 0;
        {
            const int r = 10;   //(blur-kernel-radius)
            unsigned int MinVoteVal = VotingSpace[MinIndex].size();
            for( int i=0; i<360; ++i )
            {
                //blur
                unsigned int Sum = 0;
                for( int k=i-r; k<=i+r; ++k ){  Sum += VotingSpace[ (k<0 ? k+360 : (k>=360 ? k-360 : k)) ].size();  }
                BlurredVotes[i] = (int)( 0.5 + (double)Sum / (2*r+1) );
                //find min
                if( MinVoteVal > VotingSpace[i].size() ){   MinVoteVal = VotingSpace[i].size(); MinIndex = i;   }
            }
        }

        //Find Pixel-Groups
        //  Search is started from the bin with the fewest votes.
        //  (Expect the starting bin to not belong to any group.)
        std::vector<cv::Point> Pixels_Voted_to_SameLine;
        const int ThreshOffset = 5;
        for( int i=0; i<360; ++i )
        {
            int k = (MinIndex + i)%360;
            if( VotingSpace[k].size() <= BlurredVotes[k]+ThreshOffset )
            {
                if( !Pixels_Voted_to_SameLine.empty() )
                {//The end of the group was found
                    PixelGroups.push_back( Pixels_Voted_to_SameLine );
                    Pixels_Voted_to_SameLine.clear();
                }
            }
            else
            {//Add pixels which voted to Bin[k] to current group
                for( int iPixel : VotingSpace[k] )
                {   Pixels_Voted_to_SameLine.push_back( OutlinePixels[iPixel] );    }
            }
        }
        if( !Pixels_Voted_to_SameLine.empty() )
        {   PixelGroups.push_back( Pixels_Voted_to_SameLine );  }

        //This line is just show the number of groups.
        //(When I execute this code, 4 groups found.)
        std::cout << PixelGroups.size() << " groups found." << std::endl;
    }

    {//Draw Pixel Groups to check result
        cv::Mat ShowImg = SrcImg * 0.2;
        for( int iGroup=0; iGroup<PixelGroups.size(); ++iGroup )
        {
            const cv::Vec3b DrawColor{
                unsigned char( ( (iGroup+1) & 0x4) ? 255 : 80 ),
                unsigned char( ( (iGroup+1) & 0x2) ? 255 : 80 ),
                unsigned char( ( (iGroup+1) & 0x1) ? 255 : 80 )
            };

            for( const auto &P : PixelGroups[iGroup] ){ ShowImg.at<cv::Vec3b>(P) = DrawColor;   }
        }
        cv::imshow( "GroupResult", ShowImg );
        if( cv::waitKey() == 's' ){ cv::imwrite( "GroupResult.png", ShowImg );  }
    }
    return 0;
}

Result image : 4 groups found, and pixels belong the same group were drawn in the same color. (R,G,B and Yellow)

enter image description here

fana
  • 1,370
  • 2
  • 7
  • Thank you so, so much for the implementation. May I ask what does the BlurredVotes do? –  May 13 '22 at 00:15
  • 1
    I implemented `adaptiveThreshold` for my Histogram. (See cv.adaptiveThreshold() for the meaning of the blurred data. Usage of blurred data is same as it.) – fana May 13 '22 at 00:54
  • Once again, thank you so much for your help. I've got some questions, if you don't mind: 1) Why add 0.5 in `BlurredVotes[I] = (int)(0.5 + (double) Sum/(2*r + 1))`? 2) Why perform adaptive thresholding on the votes for edge direction? Shouldn’t adaptive thresholding be used to separate foreground and background using pixel intensity? 3) Why does `VotingSpace[k].size() >= BlurredVotes[k] + ThreshOffset` (the else statement) mean the pixels voted for the same line? 4) Why do we start searching for the pixel groups at the index with the lowest number of votes? –  May 13 '22 at 14:09
  • 1
    1) Rounding. e.g. if average is 0.6, result of convert to integer becomes to 1. When without this 0.5, it becomes to 0. – fana May 13 '22 at 15:52
  • 1
    2) I would like to find the peaks(local maximums) from the histogram. "Peak" is a place where there are more votes than the surrounding area. Here, I used local average value as votes of surrounding area (and this becomes to same as adaptiveThresh). – fana May 13 '22 at 15:58
  • 1
    3) In this code, peak(group) is searched as "A bunch of bins with more votes than surrounding". e.g. While searching, when the state that "Bin[5] and Bin[6] and Bin[7] have more votes than there surrounding area, but Bin[8](and Bin[4] too) is not so" is found, a Peak consists of {Bin[5], Bin[6], and Bin[7]} . – fana May 13 '22 at 16:24
  • 1
    4) In this example case, if search is started from Bin[6], it will take some ingenuity to produce the result "a Peak consists of {Bin[5], Bin[6], and Bin[7]}". Why start from the bin with the fewest votes is to avoid this hassle (As written as comment in the code, if start bin is not belong to any peak(group), this can be avoided). – fana May 13 '22 at 16:24
  • 1
    1) is a very trivial matter. 2),3), and 4) are my heuristic way. They are all just sample implement. – fana May 13 '22 at 16:35
  • For some reason, I'm always getting more than four groups. I've followed your implementation without huge changes. –  May 15 '22 at 23:14
  • I'm performing my gradient calculation on my masked image (it shouldn't make a difference which image I perform it on because the edge direction is the same), but I have 24 groups. I tried performing my gradient calculation on my original image and the groups get worse. –  May 16 '22 at 02:09
  • 1
    I used the value 5 for `ThreshOffset`, but this is not some well considered value. If this value is too small for your real data, many undesired groups will be extracted. This may become the reason. Check what happened. e.g. plot your Histogram and blurred Histogram into single chart, to check what happened for bin ranges of your groups. – fana May 16 '22 at 02:55
  • 1
    By the way, when many groups are detected, it may be possible to simply select only the four with the highest voting values. – fana May 16 '22 at 03:00
  • Wonderful. The implementation works. I’m thinking of improvements for efficiency, and I just wanted to confirm whether this suggested improvement would work. If I used the Sobel filters on the masked image, then because the pixel values are either 0 or 255, the gradient values in the x and y direction would be one of [-1020, -765, -510, -255, 0, 255, 510, 765, 1020]. Each pixel in the contour of the pool table must have a direction of either arctan(-1020/-1020), arctan(-1020/-765), ..., arctan(-1020/1020), ... arctan(-765/-1020) ... arctan(-765/1020) (and so on). –  May 16 '22 at 22:10
  • Only bins in the voting space corresponding to these arctangent values would be filled (and the rest would be zero): the histogram would have discrete spikes. Then, I don’t have to use adaptive thresholding; I could just immediately select the four largest bins from this voting space as each line group. –  May 16 '22 at 22:10
  • If you can simplify any part of process with keeping(or improving) the robustness, it will be better way for your actual problem. I would be happy if the story I wrote becomes some clue for you. Thanks. – fana May 17 '22 at 03:55
  • Prerequisite assumed in this story was "Number of long straight lines will be 4" only. So, if some other more strong prerequisites can be assumed, you may be able to consider other more efficient methods, I think. e.g. With "Shape of masked region will be roughly rectangular (not square)", some method based on principal axes of inertia may be considered. – fana May 17 '22 at 04:20