指定角度的OpenCV旋转矩形

7
我有一个二进制图像,其中只有一个形状,我想要找到最佳拟合旋转矩形(不是边界矩形)。我知道可以使用cv :: findContours()找到结果并应用cv :: minAreaRect(),但在我的情况下,由于数据存在噪点(来自MS Kinect,参见示例图片),这种方法的效果很差。相反,我使用PCA计算了我的二进制图像的主轴角度“a”,现在我想根据主轴角度a创建一个RotatedRect。
以下是我的问题和示意图:
你们有代码片段或具体建议吗?我担心必须进行许多Bresenham迭代,希望有一种聪明的方法。
顺便说一句,对于不太熟悉openCV的RotatedRect数据结构的人:它由高度、宽度、角度和中心点定义,假设中心点实际上位于矩形的中心。
谢谢!

2
你会不会先将形状旋转a度,然后再适配矩形? - Bobbi Bennett
旋转不是一个选择。不能假设形状实际上位于图像中心。它也可能位于左下角。我目前解决这个问题的想法是使用<图片宽度> Bresenham遍历,可能足够智能化,以便不扫描已经扫描过的区域,尝试最大化旋转矩形的宽度和高度。 - MShekow
“this has delivered poor results” 是什么意思?你能分享一下你正在处理的样本图像吗? - fireant
由于噪声敏感度较高,效果不佳,我将在上面的帖子中编辑一张图片。 - MShekow
事实证明旋转确实很有帮助。由于我从PCA获得了二值图像的均向量作为副产品,所以我围绕该点(角度为-a)旋转轮廓点并确定简单边框。结果“还好”,尽管它们不是完全对齐的。但是,找到的矩形的宽度和高度是100%正确的,并且我正在考虑通过使用与我的角度='a'向量正交的线进行Bresenham遍历来修复较小的错误,例如查找真正的顶部和左侧边界。 - MShekow
3个回答

6

好的,我的解决方案是:

方法:

  1. PCA, gives the angle and a first approximation for the rotatedRect's center
  2. Get the contour of the binary shape, rotate it into upright position, get min/max of X and Y coordinates to get the width and height of the bounding rect
  3. Subtract half the width (height) from maximum X (Y) to get the center point in the "upright space"
  4. Rotate this center point back by the inverse rotation matrix

    cv::RotatedRect Utilities::getBoundingRectPCA( cv::Mat& binaryImg ) {
    cv::RotatedRect result;
    
    //1. convert to matrix that contains point coordinates as column vectors
    int count = cv::countNonZero(binaryImg);
    if (count == 0) {
        std::cout << "Utilities::getBoundingRectPCA() encountered 0 pixels in binary image!" << std::endl;
        return cv::RotatedRect();
    }
    
    cv::Mat data(2, count, CV_32FC1);
    int dataColumnIndex = 0;
    for (int row = 0; row < binaryImg.rows; row++) {
        for (int col = 0; col < binaryImg.cols; col++) {
            if (binaryImg.at<unsigned char>(row, col) != 0) {
                data.at<float>(0, dataColumnIndex) = (float) col; //x coordinate
                data.at<float>(1, dataColumnIndex) = (float) (binaryImg.rows - row); //y coordinate, such that y axis goes up
                ++dataColumnIndex;
            }
        }
    }
    
    //2. perform PCA
    const int maxComponents = 1;
    cv::PCA pca(data, cv::Mat() /*mean*/, CV_PCA_DATA_AS_COL, maxComponents);
    //result is contained in pca.eigenvectors (as row vectors)
    //std::cout << pca.eigenvectors << std::endl;
    
    //3. get angle of principal axis
    float dx = pca.eigenvectors.at<float>(0, 0);
    float dy = pca.eigenvectors.at<float>(0, 1);
    float angle = atan2f(dy, dx)  / (float)CV_PI*180.0f;
    
    //find the bounding rectangle with the given angle, by rotating the contour around the mean so that it is up-right
    //easily finding the bounding box then
    cv::Point2f center(pca.mean.at<float>(0,0), binaryImg.rows - pca.mean.at<float>(1,0));
    cv::Mat rotationMatrix = cv::getRotationMatrix2D(center, -angle, 1);
    cv::Mat rotationMatrixInverse = cv::getRotationMatrix2D(center, angle, 1);
    
    std::vector<std::vector<cv::Point> > contours;
    cv::findContours(binaryImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
    if (contours.size() != 1) {
        std::cout << "Warning: found " << contours.size() << " contours in binaryImg (expected one)" << std::endl;
        return result;
    }
    
    //turn vector of points into matrix (with points as column vectors, with a 3rd row full of 1's, i.e. points are converted to extended coords)
    cv::Mat contourMat(3, contours[0].size(), CV_64FC1);
    double* row0 = contourMat.ptr<double>(0);
    double* row1 = contourMat.ptr<double>(1);
    double* row2 = contourMat.ptr<double>(2);
    for (int i = 0; i < (int) contours[0].size(); i++) {
        row0[i] = (double) (contours[0])[i].x;
        row1[i] = (double) (contours[0])[i].y;
        row2[i] = 1;
    }
    
    cv::Mat uprightContour = rotationMatrix*contourMat;
    
    //get min/max in order to determine width and height
    double minX, minY, maxX, maxY;
    cv::minMaxLoc(cv::Mat(uprightContour, cv::Rect(0, 0, contours[0].size(), 1)), &minX, &maxX); //get minimum/maximum of first row
    cv::minMaxLoc(cv::Mat(uprightContour, cv::Rect(0, 1, contours[0].size(), 1)), &minY, &maxY); //get minimum/maximum of second row
    
    int minXi = cvFloor(minX);
    int minYi = cvFloor(minY);
    int maxXi = cvCeil(maxX);
    int maxYi = cvCeil(maxY);
    
    //fill result
    result.angle = angle;
    result.size.width = (float) (maxXi - minXi);
    result.size.height = (float) (maxYi - minYi);
    
    //Find the correct center:
    cv::Mat correctCenterUpright(3, 1, CV_64FC1);
    correctCenterUpright.at<double>(0, 0) = maxX - result.size.width/2;
    correctCenterUpright.at<double>(1,0) = maxY - result.size.height/2;
    correctCenterUpright.at<double>(2,0) = 1;
    cv::Mat correctCenterMat = rotationMatrixInverse*correctCenterUpright;
    cv::Point correctCenter = cv::Point(cvRound(correctCenterMat.at<double>(0,0)), cvRound(correctCenterMat.at<double>(1,0)));
    
    result.center = correctCenter;
    
    return result;
    

    }


2
如果我正确理解您的问题,您是说使用findContoursminAreaRect方法由于嘈杂的输入数据而出现抖动/摆动。PCA对这种噪声并不更加稳健,因此我不明白为什么您认为通过这种方式找到模式的方向不会比您当前的代码糟糕。
如果您需要时间平滑性,一个常用且简单的解决方案是使用过滤器,即使是像alpha-beta filter这样非常简单的过滤器也可以给您想要的平滑效果。例如,在帧n中,您估计旋转矩形A的参数,在帧n+1中,您拥有具有估计参数B的矩形。与其画一个具有参数B的矩形,不如找到位于AB之间的C,然后在帧n+1中使用C来绘制矩形。

首先,平滑角度并不容易,因为-179°和179°非常相似,平滑后的值没有意义。其次,我确信PCA对小异常值更具有鲁棒性。毕竟,协方差矩阵应该只因轮廓内所有点而略有变化。当加入两三个噪点时,这不会对矩阵造成太大影响。但它可能会将minAreaRect()矩形旋转几度。 - MShekow

0

这里有另一种方法(仅供参考)

维基百科关于主成分分析的页面说:

PCA 可以被看作是将一个 n 维椭球拟合到数据上...

由于你的数据是二维的,因此可以使用 cv::fitEllipse 函数将椭圆拟合到你的数据上,并使用生成的 RotatedRect 的坐标来计算角度。与 cv::minAreaRect 相比,这样可以得到更好的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接