web-dev-qa-db-fra.com

Obtenez une représentation en une seule ligne pour plusieurs lignes proches par des lignes regroupées de manière ouverte

J'ai détecté des lignes dans une image et les ai dessinées dans un fichier image séparé sous OpenCv C++ à l'aide de la méthode HoughLinesP. Voici une partie de cette image résultante. Il existe en réalité des centaines de lignes minuscules et fines formant une seule et même grande ligne. 

enter image description here

Mais je veux quelques lignes qui représentent tout ce nombre. Les lignes les plus proches doivent être fusionnées pour ne former qu'une seule ligne. Par exemple, l'ensemble de lignes ci-dessus doit être représenté par 3 lignes distinctes, comme ci-dessous.

enter image description here

La sortie attendue est comme ci-dessus. Comment accomplir cette tâche.



Jusqu'à présent, les progrès résultent de la réponse d'Akarsakov.


(Les classes de lignes séparées résultantes sont dessinées dans des couleurs différentes). Notez que ce résultat est l'image complète originale sur laquelle je travaille, mais pas la section exemple que j'ai utilisée dans la question.

enter image description here

24

Si vous ne connaissez pas le nombre de lignes dans l'image, vous pouvez utiliser la fonction cv::partition pour fractionner les lignes d'un groupe d'équivalences.

Je vous suggère la procédure suivante:

  1. Fractionner vos lignes en utilisant cv::partition. Vous devez spécifier une bonne fonction de prédicat. Cela dépend vraiment des lignes que vous extrayez de l'image, mais je pense qu'il faut vérifier les conditions suivantes:

    • L'angle entre les lignes doit être assez petit (moins de 3 degrés, par exemple). Utilisez produit scalaire pour calculer le cosinus de l'angle.
    • La distance entre les centres des segments doit être inférieure à la moitié de la longueur maximale de deux segments.

Par exemple, il peut être implémenté comme suit:

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

Je suppose que vous avez vos lignes dans vector<Vec4i> lines;. Ensuite, vous devriez appeler cv::partition comme suit:

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

Vous devez appeler cv::partition une fois pour que toutes les lignes soient regroupées. Le vecteur labels stockera pour chaque ligne une étiquette de la grappe à laquelle elle appartient. Voir documentation pour cv::partition

  1. Après avoir obtenu tous les groupes de lignes, vous devez les fusionner. Je suggère de calculer l'angle moyen de toutes les lignes du groupe et d'estimer les points "frontières". Par exemple, si l’angle est nul (c’est-à-dire que toutes les lignes sont presque horizontales), il s’agit des points les plus à gauche et les plus à droite. Il ne reste plus qu’à tracer une ligne entre ces points.

J'ai remarqué que toutes les lignes de vos exemples sont horizontales ou verticales. Dans ce cas, vous pouvez calculer le point qui est la moyenne des centres et des points "frontières" de tous les segments, puis tracer une ligne horizontale ou verticale limitée par des points "frontière" à travers le point central.

Veuillez noter que cv::partition prend O (N ^ 2) temps, donc si vous traitez un très grand nombre de lignes, cela peut prendre beaucoup de temps.

J'espère que cela aidera. J'ai utilisé une telle approche pour une tâche similaire. 

23
akarsakov

Tout d’abord, je tiens à souligner que votre image d’origine est légèrement inclinée. Votre sortie attendue ne me semble donc qu’un bit. Je suppose que vous êtes d'accord avec les lignes qui ne sont pas 100% verticales dans votre sortie car elles sont légèrement en retrait de votre entrée.

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

Jusqu'ici, cela donne cette image:

Ces lignes ne forment pas des angles de 90 degrés, car l'image d'origine ne l'est pas.

Vous pouvez également choisir de combler l’écart entre les lignes avec:

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

Si ces lignes sont trop grasses, j'ai eu du succès en les éclaircissant avec:

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

6
Rick Smith

Je vous recommande d'utiliser HoughLines d'OpenCV.

void HoughLines (image InputArray, lignes OutputArray, double rho, double thêta, seuil int, double srn = 0, double stn = 0)

Vous pouvez ajuster avec rho et thêta l’orientation et la position possibles des lignes que vous souhaitez observer . Dans votre cas, thêta = 90 ° conviendrait (lignes verticales et horizontales uniquement).

Après cela, vous pouvez obtenir des équations à ligne uniques avec les coordonnées de Plücker. Et à partir de là, vous pouvez appliquer un K-mean avec 3 centres qui devraient correspondre approximativement à vos 3 lignes dans la deuxième image.

PS: Je verrai si je peux tester tout le processus avec votre image

3
AdMor

Voici un raffinement basé sur la réponse de @akarsakov . Un problème fondamental avec:

La distance entre les centres des segments doit être inférieure à la moitié de longueur maximale de deux segments.

est-ce que les longues lignes parallèles qui sont visuellement loin pourraient se retrouver dans la même classe d'équivalence (comme montré dans l'édition de OP).

Par conséquent, l’approche que j’ai trouvée satisfaisante me convenait:

  1. Construire une fenêtre (rectangle de délimitation) autour d'un line1.
  2. L'angle line2 est suffisamment proche de celui de line1 et au moins un point de line2 est à l'intérieur du rectangle englobant de line1

Souvent, une longue caractéristique linéaire de l'image qui est assez faible finit par être reconnue (HoughP, LSD) par un ensemble de segments de ligne séparés par des espaces considérables. Pour atténuer cela, notre rectangle de délimitation est construit autour d'une ligne étendue dans les deux directions, l'extension étant définie par une fraction de la largeur de la ligne d'origine.

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 sont les suivants:

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

Pour partager, appelez:

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

Maintenant, afin de réduire chaque classe d'équivalence à une seule ligne, nous construisons un nuage de points et trouvons un ajustement de ligne:

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

Manifestation:

 Original image

Ligne partitionnée détectée (avec de petites lignes filtrées):  enter image description here

Réduit:  enter image description here

Code de démonstration:

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

Vous pouvez fusionner plusieurs lignes proches en une seule ligne en regroupant des lignes à l'aide de rho et thêta, puis en prenant la moyenne de rho et thêta.

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

vous pouvez régler houghThreshold, rho_threshold et theta_threshold selon votre application.

    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 a fait valoir un bon argument. Pour des lignes comme celle-ci, c’est-à-dire extraites probablement d’images de type tableau/formulaire, vous devez tirer pleinement parti du fait que de nombreux segments de ligne capturés par la transformation de Hough à partir des mêmes lignes ont des caractéristiques très similaires.\rho et\theta. 

Après avoir regroupé ces segments de ligne en fonction de leurs\rho et\theta, vous pouvez appliquer un ajustement de ligne 2D pour obtenir une estimation des lignes vraies dans une image. 

Il y a un papier décrivant cette idée et faisant de nouvelles hypothèses sur les lignes d'une page. 

HTH.

0
galactica