web-dev-qa-db-ja.com

opencvでクラスター化された複数の近接線の単一線表現を取得する

画像の線を検出し、HoughLinesPメソッドを使用してOpenCv C++の別の画像ファイルに描きました。以下は、その結果の画像の一部です。実際には、何百もの細くて細い線があり、1つの大きな線を形成しています。

enter image description here

しかし、私はそれらすべての行数を表す単一の数行が必要です。近い行は1つの行を形成するために一緒にマージする必要があります。たとえば、上記の一連の行は、以下のように3つの別々の行で表す必要があります。

enter image description here

予想される出力は上記のとおりです。このタスクを実行する方法。



これまでのところ、進歩はアカルサコフの答えから生じています。


(結果として得られた線のクラスは異なる色で描画されます)。この結果は私が作業している元の完全な画像ですが、質問で使用したサンプルセクションではないことに注意してください

enter image description here

27

画像の行数がわからない場合は、 cv::partition 関数を使用して、同等グループの行を分割できます。

次の手順をお勧めします。

  1. cv::partitionを使用して行を分割します。適切な述語関数を指定する必要があります。実際に画像から抽出した線に依存しますが、以下の条件をチェックする必要があると思います:

    • 線間の角度は非常に小さくなければなりません(たとえば、3度未満)。 dot product を使用して、角度の余弦を計算します。
    • セグメントの中心間の距離は、2つのセグメントの最大長の半分未満でなければなりません。

たとえば、次のように実装できます。

bool isEqual(const Vec4i& _l1, const Vec4i& _l2)
{
    Vec4i l1(_l1), l2(_l2);

    float length1 = sqrtf((l1[2] - l1[0])*(l1[2] - l1[0]) + (l1[3] - l1[1])*(l1[3] - l1[1]));
    float length2 = sqrtf((l2[2] - l2[0])*(l2[2] - l2[0]) + (l2[3] - l2[1])*(l2[3] - l2[1]));

    float product = (l1[2] - l1[0])*(l2[2] - l2[0]) + (l1[3] - l1[1])*(l2[3] - l2[1]);

    if (fabs(product / (length1 * length2)) < cos(CV_PI / 30))
        return false;

    float mx1 = (l1[0] + l1[2]) * 0.5f;
    float mx2 = (l2[0] + l2[2]) * 0.5f;

    float my1 = (l1[1] + l1[3]) * 0.5f;
    float my2 = (l2[1] + l2[3]) * 0.5f;
    float dist = sqrtf((mx1 - mx2)*(mx1 - mx2) + (my1 - my2)*(my1 - my2));

    if (dist > std::max(length1, length2) * 0.5f)
        return false;

    return true;
}

vector<Vec4i> lines;に行があると思います。次に、次のようにcv::partitionを呼び出す必要があります。

vector<Vec4i> lines;
std::vector<int> labels;
int numberOfLines = cv::partition(lines, labels, isEqual);

cv::partitionを一度呼び出すと、すべての行がクラスター化されます。 Vector labelsは、それが属するクラスターの各ラインラベルを格納します。 cv::partitionについては documentation を参照してください

  1. 行のすべてのグループを取得したら、それらをマージする必要があります。グループ内のすべての線の平均角度を計算し、「境界」点を推定することをお勧めします。たとえば、角度がゼロの場合(つまり、すべての線がほぼ水平である場合)は、左端のポイントと右端のポイントになります。この点の間に線を引くだけです。

あなたの例のすべての線が水平または垂直であることに気づきました。このような場合、すべてのセグメントの中心と「境界」ポイントの平均であるポイントを計算してから、中心ポイントを通る「境界」ポイントによって制限される水平線または垂直線を描くことができます。

cv::partitionはO(N ^ 2)時間かかるため、膨大な数の行を処理する場合、時間がかかる場合があることに注意してください。

お役に立てば幸いです。私は同様のタスクにそのようなアプローチを使用しました。

27
akarsakov

最初に、元の画像が少し角度があることに注意したいので、期待される出力はbitに思えます。入力ではわずかにずれているため、出力で100%垂直ではない線でも問題ないと思います。

Mat image;
Mat binary = image > 125;  // Convert to binary image

// Combine similar lines
int size = 3;
Mat element = getStructuringElement( MORPH_ELLIPSE, Size( 2*size + 1, 2*size+1 ), Point( size, size ) );
morphologyEx( mask, mask, MORPH_CLOSE, element );

これまでのところ、これはこの画像をもたらします: 

元の画像とは異なるため、これらの線は90度の角度ではありません。

また、行間のギャップを閉じることを選択できます。

Mat out = Mat::zeros(mask.size(), mask.type());

vector<Vec4i> lines;
HoughLinesP(mask, lines, 1, CV_PI/2, 50, 50, 75);
for( size_t i = 0; i < lines.size(); i++ )
{
    Vec4i l = lines[i];
    line( out, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(255), 5, CV_AA);
}

これらの線が太すぎる場合、私はそれらを薄くすることに成功しました:

size = 15;
Mat eroded;
cv::Mat erodeElement = getStructuringElement( MORPH_ELLIPSE, cv::Size( size, size ) );
erode( mask, eroded, erodeElement );

6
Rick Smith

これは、@ akarsakovの回答に基づいた改良版です。基本的な問題:

セグメントの中心間の距離は、2つのセグメントの最大長の半分未満でなければなりません。

視覚的に遠い平行な長い線が同じ等価クラスになる可能性があるということです(OPの編集で示されているように)。

したがって、私が合理的に機能していると私が見つけたアプローチ:

  1. line1の周りにウィンドウ(外接する四角形)を作成します。
  2. line2の角度はline1の角度に十分近く、line2の少なくとも1点はline1の外接長方形の内側にあります

多くの場合、非常に弱い画像内の長い線形の特徴は、それらの間にかなりのギャップがある一連のラインセグメントによって最終的に認識されます(HoughP、LSD)。これを軽減するために、境界長方形は、両方向に拡張された線の周囲に作成されます。拡張は、元の線幅の一部によって定義されます。

bool extendedBoundingRectangleLineEquivalence(const Vec4i& _l1, const Vec4i& _l2, float extensionLengthFraction, float maxAngleDiff, float boundingRectangleThickness){

    Vec4i l1(_l1), l2(_l2);
    // extend lines by percentage of line width
    float len1 = sqrtf((l1[2] - l1[0])*(l1[2] - l1[0]) + (l1[3] - l1[1])*(l1[3] - l1[1]));
    float len2 = sqrtf((l2[2] - l2[0])*(l2[2] - l2[0]) + (l2[3] - l2[1])*(l2[3] - l2[1]));
    Vec4i el1 = extendedLine(l1, len1 * extensionLengthFraction);
    Vec4i el2 = extendedLine(l2, len2 * extensionLengthFraction);

    // reject the lines that have wide difference in angles
    float a1 = atan(linearParameters(el1)[0]);
    float a2 = atan(linearParameters(el2)[0]);
    if(fabs(a1 - a2) > maxAngleDiff * M_PI / 180.0){
        return false;
    }

    // calculate window around extended line
    // at least one point needs to inside extended bounding rectangle of other line,
    std::vector<Point2i> lineBoundingContour = boundingRectangleContour(el1, boundingRectangleThickness/2);
    return
        pointPolygonTest(lineBoundingContour, cv::Point(el2[0], el2[1]), false) == 1 ||
        pointPolygonTest(lineBoundingContour, cv::Point(el2[2], el2[3]), false) == 1;
}

ここで、linearParameters, extendedLine, boundingRectangleContourは次のとおりです。

Vec2d linearParameters(Vec4i line){
    Mat a = (Mat_<double>(2, 2) <<
                line[0], 1,
                line[2], 1);
    Mat y = (Mat_<double>(2, 1) <<
                line[1],
                line[3]);
    Vec2d mc; solve(a, y, mc);
    return mc;
}

Vec4i extendedLine(Vec4i line, double d){
    // oriented left-t-right
    Vec4d _line = line[2] - line[0] < 0 ? Vec4d(line[2], line[3], line[0], line[1]) : Vec4d(line[0], line[1], line[2], line[3]);
    double m = linearParameters(_line)[0];
    // solution of pythagorean theorem and m = yd/xd
    double xd = sqrt(d * d / (m * m + 1));
    double yd = xd * m;
    return Vec4d(_line[0] - xd, _line[1] - yd , _line[2] + xd, _line[3] + yd);
}

std::vector<Point2i> boundingRectangleContour(Vec4i line, float d){
    // finds coordinates of perpendicular lines with length d in both line points
    // https://math.stackexchange.com/a/2043065/183923

    Vec2f mc = linearParameters(line);
    float m = mc[0];
    float factor = sqrtf(
        (d * d) / (1 + (1 / (m * m)))
    );

    float x3, y3, x4, y4, x5, y5, x6, y6;
    // special case(vertical perpendicular line) when -1/m -> -infinity
    if(m == 0){
        x3 = line[0]; y3 = line[1] + d;
        x4 = line[0]; y4 = line[1] - d;
        x5 = line[2]; y5 = line[3] + d;
        x6 = line[2]; y6 = line[3] - d;
    } else {
        // slope of perpendicular lines
        float m_per = - 1/m;

        // y1 = m_per * x1 + c_per
        float c_per1 = line[1] - m_per * line[0];
        float c_per2 = line[3] - m_per * line[2];

        // coordinates of perpendicular lines
        x3 = line[0] + factor; y3 = m_per * x3 + c_per1;
        x4 = line[0] - factor; y4 = m_per * x4 + c_per1;
        x5 = line[2] + factor; y5 = m_per * x5 + c_per2;
        x6 = line[2] - factor; y6 = m_per * x6 + c_per2;
    }

    return std::vector<Point2i> {
        Point2i(x3, y3),
        Point2i(x4, y4),
        Point2i(x6, y6),
        Point2i(x5, y5)
    };
}

パーティショニングするには、次を呼び出します。

std::vector<int> labels;
int equilavenceClassesCount = cv::partition(linesWithoutSmall, labels, [](const Vec4i l1, const Vec4i l2){
    return extendedBoundingRectangleLineEquivalence(
        l1, l2,
        // line extension length - as fraction of original line width
        0.2,
        // maximum allowed angle difference for lines to be considered in same equivalence class
        2.0,
        // thickness of bounding rectangle around each line
        10);
});

ここで、各等価クラスを単一の線に減らすために、それから点群を構築し、線の適合を見つけます。

// fit line to each equivalence class point cloud
std::vector<Vec4i> reducedLines = std::accumulate(pointClouds.begin(), pointClouds.end(), std::vector<Vec4i>{}, [](std::vector<Vec4i> target, const std::vector<Point2i>& _pointCloud){
    std::vector<Point2i> pointCloud = _pointCloud;

    //lineParams: [vx,vy, x0,y0]: (normalized vector, point on our contour)
    // (x,y) = (x0,y0) + t*(vx,vy), t -> (-inf; inf)
    Vec4f lineParams; fitLine(pointCloud, lineParams, CV_DIST_L2, 0, 0.01, 0.01);

    // derive the bounding xs of point cloud
    decltype(pointCloud)::iterator minXP, maxXP;
    std::tie(minXP, maxXP) = std::minmax_element(pointCloud.begin(), pointCloud.end(), [](const Point2i& p1, const Point2i& p2){ return p1.x < p2.x; });

    // derive y coords of fitted line
    float m = lineParams[1] / lineParams[0];
    int y1 = ((minXP->x - lineParams[2]) * m) + lineParams[3];
    int y2 = ((maxXP->x - lineParams[2]) * m) + lineParams[3];

    target.Push_back(Vec4i(minXP->x, y1, maxXP->x, y2));
    return target;
});

デモンストレーション:

Original image

検出された分割線(小さな線が除外されています): enter image description here

減少: - enter image description here

デモコード:

int main(int argc, const char* argv[]){

    if(argc < 2){
        std::cout << "img filepath should be present in args" << std::endl;
    }

    Mat image = imread(argv[1]);
    Mat smallerImage; resize(image, smallerImage, cv::Size(), 0.5, 0.5, INTER_CUBIC);
    Mat target = smallerImage.clone();

    namedWindow("Detected Lines", WINDOW_NORMAL);
    namedWindow("Reduced Lines", WINDOW_NORMAL);
    Mat detectedLinesImg = Mat::zeros(target.rows, target.cols, CV_8UC3);
    Mat reducedLinesImg = detectedLinesImg.clone();

    // delect lines in any reasonable way
    Mat grayscale; cvtColor(target, grayscale, CV_BGRA2GRAY);
    Ptr<LineSegmentDetector> detector = createLineSegmentDetector(LSD_REFINE_NONE);
    std::vector<Vec4i> lines; detector->detect(grayscale, lines);

    // remove small lines
    std::vector<Vec4i> linesWithoutSmall;
    std::copy_if (lines.begin(), lines.end(), std::back_inserter(linesWithoutSmall), [](Vec4f line){
        float length = sqrtf((line[2] - line[0]) * (line[2] - line[0])
                             + (line[3] - line[1]) * (line[3] - line[1]));
        return length > 30;
    });

    std::cout << "Detected: " << linesWithoutSmall.size() << std::endl;

    // partition via our partitioning function
    std::vector<int> labels;
    int equilavenceClassesCount = cv::partition(linesWithoutSmall, labels, [](const Vec4i l1, const Vec4i l2){
        return extendedBoundingRectangleLineEquivalence(
            l1, l2,
            // line extension length - as fraction of original line width
            0.2,
            // maximum allowed angle difference for lines to be considered in same equivalence class
            2.0,
            // thickness of bounding rectangle around each line
            10);
    });

    std::cout << "Equivalence classes: " << equilavenceClassesCount << std::endl;

    // grab a random colour for each equivalence class
    RNG rng(215526);
    std::vector<Scalar> colors(equilavenceClassesCount);
    for (int i = 0; i < equilavenceClassesCount; i++){
        colors[i] = Scalar(rng.uniform(30,255), rng.uniform(30, 255), rng.uniform(30, 255));;
    }

    // draw original detected lines
    for (int i = 0; i < linesWithoutSmall.size(); i++){
        Vec4i& detectedLine = linesWithoutSmall[i];
        line(detectedLinesImg,
             cv::Point(detectedLine[0], detectedLine[1]),
             cv::Point(detectedLine[2], detectedLine[3]), colors[labels[i]], 2);
    }

    // build point clouds out of each equivalence classes
    std::vector<std::vector<Point2i>> pointClouds(equilavenceClassesCount);
    for (int i = 0; i < linesWithoutSmall.size(); i++){
        Vec4i& detectedLine = linesWithoutSmall[i];
        pointClouds[labels[i]].Push_back(Point2i(detectedLine[0], detectedLine[1]));
        pointClouds[labels[i]].Push_back(Point2i(detectedLine[2], detectedLine[3]));
    }

    // fit line to each equivalence class point cloud
    std::vector<Vec4i> reducedLines = std::accumulate(pointClouds.begin(), pointClouds.end(), std::vector<Vec4i>{}, [](std::vector<Vec4i> target, const std::vector<Point2i>& _pointCloud){
        std::vector<Point2i> pointCloud = _pointCloud;

        //lineParams: [vx,vy, x0,y0]: (normalized vector, point on our contour)
        // (x,y) = (x0,y0) + t*(vx,vy), t -> (-inf; inf)
        Vec4f lineParams; fitLine(pointCloud, lineParams, CV_DIST_L2, 0, 0.01, 0.01);

        // derive the bounding xs of point cloud
        decltype(pointCloud)::iterator minXP, maxXP;
        std::tie(minXP, maxXP) = std::minmax_element(pointCloud.begin(), pointCloud.end(), [](const Point2i& p1, const Point2i& p2){ return p1.x < p2.x; });

        // derive y coords of fitted line
        float m = lineParams[1] / lineParams[0];
        int y1 = ((minXP->x - lineParams[2]) * m) + lineParams[3];
        int y2 = ((maxXP->x - lineParams[2]) * m) + lineParams[3];

        target.Push_back(Vec4i(minXP->x, y1, maxXP->x, y2));
        return target;
    });

    for(Vec4i reduced: reducedLines){
        line(reducedLinesImg, Point(reduced[0], reduced[1]), Point(reduced[2], reduced[3]), Scalar(255, 255, 255), 2);
    }

    imshow("Detected Lines", detectedLinesImg);
    imshow("Reduced Lines", reducedLinesImg);
    waitKey();

    return 0;
}
4
ambientlight

OpenCVのHoughLinesを使用することをお勧めします。

void HoughLines(InputArray image、OutputArray lines、double rho、double theta、int threshold、double srn = 0、double stn = 0)

Rhoおよびthetaを使用して、観察したいラインの可能な方向と位置を調整できます。あなたの場合、シータ= 90°で十分です(垂直線と水平線のみ)。

この後、Plücker座標を使用した一意の線方程式を取得できます。そしてそこから、2番目の画像の約3行に適合する3つの中心を持つK平均を適用できます。

PS:私はあなたのイメージでプロセス全体をテストできるかどうかを確認します

3
AdMor

Rhoとthetaを使用して線をクラスタリングし、最後にrhoとthetaの平均を取ることにより、複数の近い線を1つの線にマージできます。

    void contourLines(vector<cv::Vec2f> lines, const float rho_threshold, const float theta_threshold, vector< cv::Vec2f > &combinedLines)
{
    vector< vector<int> > combineIndex(lines.size());

    for (int i = 0; i < lines.size(); i++)
    {
        int index = i;
        for (int j = i; j < lines.size(); j++)
        {
            float distanceI = lines[i][0], distanceJ = lines[j][0];
            float slopeI = lines[i][1], slopeJ = lines[j][1];
            float disDiff = abs(distanceI - distanceJ);
            float slopeDiff = abs(slopeI - slopeJ);

            if (slopeDiff < theta_max && disDiff < rho_max)
            {
                bool isCombined = false;
                for (int w = 0; w < i; w++)
                {
                    for (int u = 0; u < combineIndex[w].size(); u++)
                    {
                        if (combineIndex[w][u] == j)
                        {
                            isCombined = true;
                            break;
                        }
                        if (combineIndex[w][u] == i)
                            index = w;
                    }
                    if (isCombined)
                        break;
                }
                if (!isCombined)
                    combineIndex[index].Push_back(j);
            }
        }
    }

    for (int i = 0; i < combineIndex.size(); i++)
    {
        if (combineIndex[i].size() == 0)
            continue;
        cv::Vec2f line_temp(0, 0);
        for (int j = 0; j < combineIndex[i].size(); j++) {
            line_temp[0] += lines[combineIndex[i][j]][0];
            line_temp[1] += lines[combineIndex[i][j]][1];
        }
        line_temp[0] /= combineIndex[i].size();
        line_temp[1] /= combineIndex[i].size();
        combinedLines.Push_back(line_temp);
    }
}

関数呼び出しアプリケーションに応じて、houghThreshold、rho_threshold、theta_thresholdを調整できます。

    HoughLines(Edge, lines_t, 1, CV_PI / 180, houghThreshold, 0, 0);

    float rho_threshold= 15;
    float theta_threshold = 3*DEGREES_TO_RADIANS;
    vector< cv::Vec2f > lines;
    contourCluster(lines_t, rho_max, theta_max, lines);

lines before clustering

lines after clustering

2
C_Raj

@C_Rajは良い点を作りました。このような線、つまりテーブル/フォームのような画像から抽出された可能性が最も高いため、同じ線からハフ変換によってキャプチャされた線セグメントの多くは非常に類似しているという事実を最大限に活用する必要があります\ rhoおよび\ theta。

\ rhoおよび\ thetaに基づいてこれらのラインセグメントをクラスター化した後、2Dラインフィッティングを適用して、画像内の実際のラインの推定値を取得できます。

このアイデアを説明する paper があり、ページ内の行をさらに仮定しています。

HTH。

1
galactica