wake-up-neo.com

Erhalten Sie eine einzeilige Darstellung für mehrere in opencv zusammengefasste close by-Linien

Ich habe Linien in einem Bild erkannt und sie mit einer HoughLinesP-Methode in eine separate Bilddatei in OpenCv C++ gezeichnet. Folgendes ist ein Teil des resultierenden Bildes. Es gibt tatsächlich Hunderte kleiner und dünner Linien, die eine große Einzellinie bilden. 

enter image description here

Aber ich möchte einzelne Zeilen, die all diese Zeilenanzahl darstellen. Engere Zeilen sollten zu einer einzigen Zeile zusammengefügt werden. Zum Beispiel sollte der oben genannte Satz von Zeilen durch nur drei separate Zeilen dargestellt werden.

enter image description here

Die erwartete Ausgabe ist wie oben. Wie man diese Aufgabe erledigt.



Der bisherige Fortschritt ergibt sich aus der Antwort von Akarsakov.


(Separate Linienklassen wurden in verschiedenen Farben gezeichnet). Beachten Sie, dass dieses Ergebnis das ursprüngliche vollständige Bild ist, an dem ich arbeite, nicht jedoch der Beispielabschnitt, den ich in der Frage verwendet hatte

enter image description here

24

Wenn Sie die Anzahl der Zeilen im Bild nicht kennen, können Sie die Funktion cv::partition verwenden, um die Zeilen in einer Äquivalenzgruppe aufzuteilen.

Ich empfehle Ihnen folgendes Verfahren:

  1. Teilen Sie Ihre Zeilen mit cv::partition auf. Sie müssen eine gute Prädikatfunktion angeben. Es hängt wirklich von den Zeilen ab, die Sie aus dem Bild extrahieren, aber ich denke, es sollte die folgenden Bedingungen prüfen:

    • Der Winkel zwischen den Linien sollte ziemlich klein sein (zum Beispiel weniger als 3 Grad). Verwenden Sie dot product , um den Cosinus des Winkels zu berechnen.
    • Der Abstand zwischen den Mittelpunkten der Segmente sollte weniger als die Hälfte der maximalen Länge von zwei Segmenten betragen.

Zum Beispiel kann es wie folgt implementiert werden:

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

Ich denke, Sie haben Ihre Zeilen in vector<Vec4i> lines;. Als Nächstes sollten Sie cv::partition wie folgt aufrufen:

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

Sie müssen einmal cv::partition aufrufen, und alle Zeilen werden gruppiert. Der Vektor labels speichert für jede Zeilenbezeichnung des Clusters, zu dem er gehört. Siehe Dokumentation für cv::partition

  1. Nachdem Sie alle Zeilengruppen erhalten haben, sollten Sie sie zusammenführen. Ich empfehle, den durchschnittlichen Winkel aller Linien in der Gruppe zu berechnen und "Grenzpunkte" zu schätzen. Wenn der Winkel beispielsweise Null ist (d. H. Alle Linien fast horizontal sind), wäre dies der am weitesten links und ganz rechts liegende Punkt. Es bleibt nur noch eine Linie zwischen diesen Punkten zu zeichnen.

Ich habe festgestellt, dass alle Zeilen in Ihren Beispielen horizontal oder vertikal sind. In diesem Fall können Sie einen Punkt berechnen, der den Durchschnitt aller Segment- und "Rand" -Punkte des Segments darstellt, und dann einfach eine horizontale oder vertikale Linie zeichnen, die durch "Rand" -Punkte durch den Mittelpunkt begrenzt ist.

Bitte beachten Sie, dass cv::partition O (N ^ 2) Zeit benötigt. Wenn Sie eine große Anzahl von Zeilen verarbeiten, kann dies viel Zeit in Anspruch nehmen.

Ich hoffe es wird helfen. Ich habe diesen Ansatz für eine ähnliche Aufgabe verwendet. 

23
akarsakov

Als Erstes möchte ich darauf hinweisen, dass Ihr Originalbild in einem leichten Winkel steht, und Ihre erwartete Ausgabe scheint mir nur ein bit zu sein. Ich gehe davon aus, dass Sie mit Zeilen zufrieden sind, die in Ihrer Ausgabe nicht zu 100% vertikal sind, da sie bei Ihrer Eingabe etwas abnehmen.

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

Bisher ergibt sich dieses Bild:

Diese Linien stehen nicht in einem Winkel von 90 Grad, da das Originalbild nicht ist.

Sie können auch die Lücke zwischen den Zeilen schließen mit:

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

Wenn diese Linien zu dick sind, hatte ich Erfolg, sie mit zu verdünnen:

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

6
Rick Smith

Ich würde empfehlen, dass Sie HoughLines von OpenCV verwenden.

void HoughLines (InputArray-Bild, OutputArray-Zeilen, Double Rho, Double Theta, Int-Schwelle, Double SRN = 0, Double STN = 0)

Sie können mit Rho und Theta die mögliche Ausrichtung und Position der Linien einstellen, die Sie beobachten möchten. In Ihrem Fall wäre Theta = 90 ° in Ordnung (nur vertikale und horizontale Linien).

Danach können Sie eindeutige Liniengleichungen mit Plücker-Koordinaten erhalten. Von hier aus können Sie einen K-Mittelwert mit 3 Zentren anwenden, der ungefähr in Ihre 3 Zeilen im zweiten Bild passen sollte.

PS: Ich werde sehen, ob ich den gesamten Prozess mit Ihrem Image testen kann

3
AdMor

Hier ist eine Verfeinerung, die auf der Antwort von @akarsakov aufbaut .. Ein grundlegendes Problem mit:

Der Abstand zwischen den Mittelpunkten der Segmente sollte weniger als die Hälfte von .__ betragen. maximale Länge von zwei Segmenten.

ist, dass parallele lange Linien, die visuell weit entfernt sind, in derselben Äquivalenzklasse enden könnten (wie in OPs Edit gezeigt).

Daher der Ansatz, den ich für sinnvoll befunden habe:

  1. Konstruieren Sie ein Fenster (Begrenzungsrechteck) um einen line1.
  2. Der Winkel line2 liegt nahe genug an line1 und mindestens ein Punkt von line2 befindet sich innerhalb des Begrenzungsrechtecks ​​von line1

Oft wird ein langes lineares Merkmal im Bild, das ziemlich schwach ist, am Ende von einer Reihe von Liniensegmenten mit beträchtlichen Lücken erkannt (HoughP, LSD). Um dies zu vermeiden, ist unser Begrenzungsrechteck um eine in beiden Richtungen verlängerte Linie aufgebaut, wobei die Ausdehnung durch einen Bruchteil der ursprünglichen Linienbreite definiert ist.

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

wo linearParameters, extendedLine, boundingRectangleContour folgt:

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

Um zu teilen, rufen Sie an:

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

Um nun jede Äquivalenzklasse auf eine einzelne Linie zu reduzieren, bauen wir daraus eine Punktwolke auf und finden eine passende Linie:

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

Demonstration:

 Original image

Erkannte partitionierte Zeile (mit kleinen Zeilen herausgefiltert):  enter image description here

Reduziert:  enter image description here

Demonstrationscode:

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

Sie können mehrere nahe Linien zu einer Linie zusammenfügen, indem Sie die Zeilen mit Rho und Theta gruppieren und schließlich den Durchschnitt von Rho und Theta ermitteln.

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

funktionsaufruf Sie können houghThreshold, rho_threshold und theta_threshold an Ihre Anwendung anpassen.

    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

1
C_Raj

@C_Raj machte einen guten Punkt, für Linien wie diese, das heißt, höchstwahrscheinlich aus tabellen-/formularähnlichen Bildern extrahiert, sollten Sie die Tatsache voll ausnutzen, dass viele der von Hough-Transformation erfassten Liniensegmente aus den gleichen Linien sehr ähnlich sind\rho und\theta. 

Nach dem Gruppieren dieser Liniensegmente basierend auf\rho und\theta können Sie die Anpassung der 2D-Linien anwenden, um eine Schätzung der wahren Linien in einem Bild zu erhalten. 

Es gibt ein Papier , das diese Idee beschreibt und weitere Annahmen über die Zeilen einer Seite macht. 

HTH.

0
galactica