有没有办法保证使用 cv2.HoughLines() 检测到一定数量的行?

Is there a way to guarantee a certain number of lines detected with cv2.HoughLines()?

这道题是我之前question asking about how to detect a pool table's corners. I have found the outline的一道题table的延伸题,我已经设法在轮廓上应用了霍夫变换。这个霍夫变换的结果如下:

不幸的是,Hough 变换 return 一条 table 边的多条线。我希望 Hough 变换为 return 四行,每行对应于给定池 table 的任何图像的 table 的边缘。我不想手动调整 Hough 变换方法的参数(因为池 table 的每个图像的池 table 的轮廓可能不同)。有什么方法可以保证 cv2.HoughLines()?

生成四行

提前致谢。

编辑

根据@fana 的评论,我使用以下代码创建了梯度方向直方图。我仍然不完全确定如何从此直方图中获得四行。

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)

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.

我试了一下

因为我不懂python,下面的示例代码是C++的。不过做了什么都写成注释了,看来你也能看懂了。

本样本包含以下内容:

  • 提取水池的轮廓table。
  • 创建Gradient-Direction 直方图(使用 Sobel 滤波器估计梯度)。
  • 根据直方图峰值查找像素组。

此示例不包括 line-fitting 过程。

看分组结果,似乎有些像素会因为线拟合而变得离群。因此,我认为最好使用一些稳健的拟合方法(例如M-estimator,RANSAC)。

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;
}

结果图片: 找到 4 组,并且属于同一组的像素以相同的颜色绘制。 (红、绿、蓝和黄色)