Algorytm wykrywania rogów arkusza papieru na zdjęciu

98

Jaki jest najlepszy sposób na wykrycie narożników faktury / paragonu / kartki papieru na zdjęciu? Ma to służyć do późniejszej korekty perspektywy, przed OCR.

Moje obecne podejście jest następujące:

RGB> Gray> Canny Edge Detection with proging> Dilate (1)> Remove small objects (6)> clear border objects> pick larges Blog based on Convex Area. > [wykrywanie narożników - nie zaimplementowano]

Nie mogę pomóc, ale myślę, że musi istnieć bardziej solidne, „inteligentne” / statystyczne podejście do obsługi tego typu segmentacji. Nie mam wielu przykładów szkoleniowych, ale prawdopodobnie mógłbym zebrać razem 100 zdjęć.

Szerszy kontekst:

Używam Matlaba do prototypowania i planuję wdrożenie systemu w OpenCV i Tesserect-OCR. Jest to pierwszy z wielu problemów związanych z przetwarzaniem obrazu, które muszę rozwiązać dla tej konkretnej aplikacji. Dlatego chcę rozwinąć własne rozwiązanie i ponownie zapoznać się z algorytmami przetwarzania obrazu.

Oto przykładowy obraz, który powinien obsługiwać algorytm: Jeśli chcesz podjąć wyzwanie, duże obrazy znajdują się pod adresem http://madteckhead.com/tmp

przypadek 1
(źródło: madteckhead.com )

przypadek 2
(źródło: madteckhead.com )

przypadek 3
(źródło: madteckhead.com )

przypadek 4
(źródło: madteckhead.com )

W najlepszym przypadku daje to:

przypadek 1 - canny
(źródło: madteckhead.com )

przypadek 1 - post canny
(źródło: madteckhead.com )

przypadek 1 - największy blog
(źródło: madteckhead.com )

Jednak łatwo zawodzi w innych przypadkach:

przypadek 2 - canny
(źródło: madteckhead.com )

przypadek 2 - post canny
(źródło: madteckhead.com )

przypadek 2 - największy blog
(źródło: madteckhead.com )

Z góry dziękuję za wszystkie świetne pomysły! Tak kocham!

EDYCJA: Hough Transform Progress

P: Jaki algorytm grupowałby linie kresek, aby znaleźć narożniki? Zgodnie z radami zawartymi w odpowiedziach udało mi się użyć transformacji Hough, wybrać linie i je filtrować. Moje obecne podejście jest raczej surowe. Założyłem, że faktura będzie zawsze mniej niż 15 stopni odbiegająca od obrazu. W takim przypadku otrzymuję rozsądne wyniki dla linii (patrz poniżej). Ale nie jestem całkowicie pewien odpowiedniego algorytmu do grupowania linii (lub głosowania) w celu ekstrapolacji na rogi. Linie Hougha nie są ciągłe. A na zaszumionych obrazach mogą występować równoległe linie, więc wymagana jest pewna forma lub metryka początku linii. Jakieś pomysły?

przypadek 1 przypadek 2 przypadek 3 przypadek 4
(źródło: madteckhead.com )

Nathan Keller
źródło
1
Tak, udało mi się to zadziałać w około 95% przypadków. Od tego czasu musiałem odłożyć kod na półkę z powodu braku czasu. Na pewnym etapie prześlę wiadomość, jeśli potrzebujesz pilnej pomocy, możesz zlecić mi to. Przepraszamy za brak dobrych obserwacji. Bardzo chciałbym wrócić do pracy nad tą funkcją.
Nathan Keller
Nathan, czy mógłbyś opublikować podsumowanie, w jaki sposób to zrobiłeś? Utknąłem w tym samym punkcie rozpoznając narożniki / zewnętrzne kontury kartek. Napotykam dokładnie te same problemy, co ty, więc byłbym bardzo zainteresowany rozwiązaniem.
Tim
6
Wszystkie obrazy w tym poście są teraz 404.
ChrisF

Odpowiedzi:

28

Jestem przyjacielem Martina, który pracował nad tym na początku tego roku. To był mój pierwszy projekt kodowania i skończył się trochę w pośpiechu, więc kod wymaga jakiegoś błędu ... dekodowania ... Dam kilka wskazówek na temat tego, co już widziałem, a potem posortuj mój kod w jutrzejszy dzień wolny.

Pierwsza wskazówka OpenCVi pythonsą świetne, przejdź do nich jak najszybciej. :RE

Zamiast usuwać małe obiekty i / lub szum, zmniejsz sprytne ograniczenia, aby akceptował więcej krawędzi, a następnie znajdź największy zamknięty kontur (w użyciu OpenCV findcontour()z kilkoma prostymi parametrami, myślę, że użyłem CV_RETR_LIST). może nadal walczyć, gdy jest na białej kartce papieru, ale zdecydowanie zapewniał najlepsze wyniki.

W przypadku Houghline2()Transform, spróbuj z the, CV_HOUGH_STANDARDa nie z the CV_HOUGH_PROBABILISTIC, da to wartości rho i theta , definiując linię we współrzędnych biegunowych, a następnie możesz zgrupować linie z pewną tolerancją.

Moje grupowanie działało jako tabela przeglądowa, dla każdej linii wyprowadzonej z transformacji hough dałoby to parę rho i theta. Jeśli te wartości mieściły się w, powiedzmy, 5% pary wartości w tabeli, zostały odrzucone, a jeśli były poza tymi 5%, do tabeli został dodany nowy wpis.

Możesz wtedy znacznie łatwiej przeprowadzić analizę równoległych linii lub odległości między liniami.

Mam nadzieję że to pomoże.

Daniel Crowley
źródło
Cześć Daniel, dzięki za zaangażowanie. Podoba mi się, że się zbliżasz. to właściwie trasa, z którą w tej chwili osiągam dobre wyniki. Był równy i przykład OpenCV, który wykrył prostokąty. Wystarczyło trochę przefiltrować wyniki. tak jak powiedziałeś, że białe na białym jest trudne do wykrycia tą metodą. Ale było to proste i mniej kosztowne podejście niż hough. Właściwie zostawiłem podejście hough z mojego algorytmu i wykonałem przybliżenie poli, spójrz na przykład kwadratów w opencv. Chciałbym zobaczyć, jak wdrażasz głosowanie hough. Z góry dziękuję, Nathan
Nathan Keller.
Miałem problemy z tym podejściem, opublikuję rozwiązanie, jeśli mogę wymyślić coś lepszego do wykorzystania w przyszłości
Anshuman Kumar
@AnshumanKumar Naprawdę potrzebuję pomocy z tym pytaniem, czy możesz mi pomóc, proszę? stackoverflow.com/questions/61216402/…
Carlos Diego
19

Grupa studentów na moim uniwersytecie niedawno zademonstrowała aplikację na iPhone'a (i aplikację Python OpenCV), którą napisali właśnie w tym celu. O ile pamiętam, kroki wyglądały mniej więcej tak:

  • Filtr mediany, aby całkowicie usunąć tekst z papieru (był to tekst odręczny na białym papierze z dość dobrym oświetleniem i może nie działać z drukowanym tekstem, działał bardzo dobrze). Powodem było to, że znacznie ułatwia to wykrywanie narożników.
  • Hough Transform dla linii
  • Znajdź szczyty w przestrzeni akumulatora Hough Transform i narysuj każdą linię na całym obrazie.
  • Przeanalizuj linie i usuń te, które są bardzo blisko siebie i znajdują się pod podobnym kątem (połącz linie w jedną). Jest to konieczne, ponieważ transformata Hougha nie jest idealna, ponieważ działa w oddzielnej przestrzeni na próbki.
  • Znajdź pary prostych, które są z grubsza równoległe i przecinają inne pary, aby zobaczyć, które proste tworzą czworokąt.

Wydawało się, że działa to całkiem dobrze i byli w stanie zrobić zdjęcie kartki papieru lub książki, wykonać wykrywanie narożników, a następnie zmapować dokument na obrazie na płaską płaszczyznę w czasie prawie rzeczywistym (była jedna funkcja OpenCV do wykonania mapowanie). Kiedy zobaczyłem, że działa, nie było OCR.

Martin Foot
źródło
Dzięki za świetne pomysły Martin. Skorzystałem z Twojej rady i wdrożyłem metodę transformacji Hough. (Zobacz wyniki powyżej). Usiłuję określić solidny algorytm, który ekstrapoluje linie, aby znaleźć przecięcia. Nie ma wielu wierszy i kilka fałszywych alarmów. Czy masz jakieś rady, jak najlepiej scalić i odrzucić linie? Jeśli Twoi uczniowie są zainteresowani, zachęcaj ich do kontaktu. Bardzo chciałbym poznać ich doświadczenia związane z uruchamianiem algorytmów na platformie mobilnej. (To mój następny cel). Wielkie dzięki za pomysły.
Nathan Keller
1
Wygląda na to, że HT dla linii działała dobrze na wszystkich obrazach oprócz drugiego, ale czy definiujesz tolerancję progową dla wartości początkowych i końcowych w akumulatorze? HT tak naprawdę nie definiuje pozycji początkowej i końcowej, a raczej wartości m i c w y = mx + c. Spójrz tutaj - zauważ, że jest to użycie współrzędnych biegunowych w akumulatorze, a nie kartezjańskich. W ten sposób możesz pogrupować linie według c, a następnie według m, aby je rozrzedzić, a wyobrażając sobie, że linie rozciągają się na cały obraz, znajdziesz bardziej przydatne przecięcia.
Martin Foot,
@MartinFoot Naprawdę potrzebuję pomocy z tym pytaniem, czy możesz mi pomóc, proszę? stackoverflow.com/questions/61216402/…
Carlos Diego
16

Oto, co wymyśliłem po krótkich eksperymentach:

import cv, cv2, numpy as np
import sys

def get_new(old):
    new = np.ones(old.shape, np.uint8)
    cv2.bitwise_not(new,new)
    return new

if __name__ == '__main__':
    orig = cv2.imread(sys.argv[1])

    # these constants are carefully picked
    MORPH = 9
    CANNY = 84
    HOUGH = 25

    img = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY)
    cv2.GaussianBlur(img, (3,3), 0, img)


    # this is to recognize white on white
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(MORPH,MORPH))
    dilated = cv2.dilate(img, kernel)

    edges = cv2.Canny(dilated, 0, CANNY, apertureSize=3)

    lines = cv2.HoughLinesP(edges, 1,  3.14/180, HOUGH)
    for line in lines[0]:
         cv2.line(edges, (line[0], line[1]), (line[2], line[3]),
                         (255,0,0), 2, 8)

    # finding contours
    contours, _ = cv2.findContours(edges.copy(), cv.CV_RETR_EXTERNAL,
                                   cv.CV_CHAIN_APPROX_TC89_KCOS)
    contours = filter(lambda cont: cv2.arcLength(cont, False) > 100, contours)
    contours = filter(lambda cont: cv2.contourArea(cont) > 10000, contours)

    # simplify contours down to polygons
    rects = []
    for cont in contours:
        rect = cv2.approxPolyDP(cont, 40, True).copy().reshape(-1, 2)
        rects.append(rect)

    # that's basically it
    cv2.drawContours(orig, rects,-1,(0,255,0),1)

    # show only contours
    new = get_new(img)
    cv2.drawContours(new, rects,-1,(0,255,0),1)
    cv2.GaussianBlur(new, (9,9), 0, new)
    new = cv2.Canny(new, 0, CANNY, apertureSize=3)

    cv2.namedWindow('result', cv2.WINDOW_NORMAL)
    cv2.imshow('result', orig)
    cv2.waitKey(0)
    cv2.imshow('result', dilated)
    cv2.waitKey(0)
    cv2.imshow('result', edges)
    cv2.waitKey(0)
    cv2.imshow('result', new)
    cv2.waitKey(0)

    cv2.destroyAllWindows()

Nie jest idealny, ale działa przynajmniej dla wszystkich próbek:

1 2 3 4

Vanuan
źródło
4
Pracuję nad podobnym projektem. Uruchamiam nad kodem i wyświetla mi się błąd „Brak modułu o nazwie cv”. Zainstalowałem wersję Open CV 2.4 i import cv2 działa dla mnie idealnie.
Navneet Singh
Czy byłbyś na tyle uprzejmy, aby zaktualizować ten kod, aby działał? pastebin.com/PMH5Y0M8 daje mi tylko czarną stronę.
7
Czy masz jakiś pomysł, jak przekształcić następujący kod do java: for line in lines[0]: cv2.line(edges, (line[0], line[1]), (line[2], line[3]), (255,0,0), 2, 8) # finding contours contours, _ = cv2.findContours(edges.copy(), cv.CV_RETR_EXTERNAL, cv.CV_CHAIN_APPROX_TC89_KCOS) contours = filter(lambda cont: cv2.arcLength(cont, False) > 100, contours) contours = filter(lambda cont: cv2.contourArea(cont) > 10000, contours)
aurelianr
Vanuan, naprawdę potrzebuję pomocy z tym pytaniem, czy możesz mi pomóc, proszę? stackoverflow.com/questions/61216402/…
Carlos Diego
9

Zamiast rozpoczynać od wykrywania krawędzi, możesz użyć wykrywania narożników.

W tym celu Marvin Framework zapewnia implementację algorytmu Moravec. Punktem wyjścia mogą być rogi dokumentów. Poniżej wyników algorytmu Moraveca:

wprowadź opis obrazu tutaj

Gabriel Ambrósio Archanjo
źródło
4

Możesz także użyć MSER (Maksymalnie stabilne regiony ekstremalne) nad wynikiem operatora Sobela, aby znaleźć stabilne obszary obrazu. Dla każdego regionu zwróconego przez MSER można zastosować wypukłe kadłub i aproksymację poli, aby uzyskać takie:

Ale ten rodzaj wykrywania jest przydatny do wykrywania na żywo więcej niż pojedynczego obrazu, który nie zawsze daje najlepszy wynik.

wynik

Flayn
źródło
1
Czy możesz podać więcej szczegółów na ten temat, być może jakiś kod, z góry dziękuję
Monty
Otrzymuję błąd w cv2.CHAIN_APPROX_SIMPLE mówiący za dużo wartości do rozpakowania. Dowolny pomysł? Używam obrazu 1024 * 1024 jako mojej próbki
Praveen
1
Dzięki wszystkim, po prostu zorientowali się zmiany składni w bieżącym OpenCV oddziału answers.opencv.org/question/40329/...
praveen
Czy MSER nie jest przeznaczony do wyodrębniania obiektów blob? Wypróbowałem to i wykrywa tylko większość tekstu
Anshuman Kumar,
3

Po wykryciu krawędzi użyj Transformacji Hougha. Następnie umieść te punkty w SVM (wspomagającej maszynie wektorowej) z ich etykietami, jeśli przykłady mają na nich gładkie linie, SVM nie będzie miał trudności z podzieleniem niezbędnych części przykładu i innych części. Moja rada dotycząca SVM, podaj parametr taki jak łączność i długość. Oznacza to, że jeśli punkty są połączone i długie, prawdopodobnie będą to linia paragonu. Następnie możesz wyeliminować wszystkie pozostałe punkty.

Hefajstos
źródło
Cześć Ares, dzięki za twoje pomysły! Zaimplementowałem transformację Hougha (patrz wyżej). Nie mogę znaleźć solidnego sposobu na znalezienie rogów, biorąc pod uwagę fałszywe alarmy i nieciągłe linie. Czy masz jakieś dalsze pomysły? Minęło trochę czasu, odkąd przyjrzałem się technikom SVM. Czy jest to podejście nadzorowane? Nie mam żadnych danych treningowych, ale mógłbym je wygenerować. Byłbym zainteresowany zbadaniem tego podejścia, ponieważ chciałbym dowiedzieć się więcej o SVM. Czy możesz polecić jakieś zasoby. Z poważaniem. Nathan
Nathan Keller
3

Tutaj masz kod @Vanuan w C ++:

cv::cvtColor(mat, mat, CV_BGR2GRAY);
cv::GaussianBlur(mat, mat, cv::Size(3,3), 0);
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Point(9,9));
cv::Mat dilated;
cv::dilate(mat, dilated, kernel);

cv::Mat edges;
cv::Canny(dilated, edges, 84, 3);

std::vector<cv::Vec4i> lines;
lines.clear();
cv::HoughLinesP(edges, lines, 1, CV_PI/180, 25);
std::vector<cv::Vec4i>::iterator it = lines.begin();
for(; it!=lines.end(); ++it) {
    cv::Vec4i l = *it;
    cv::line(edges, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(255,0,0), 2, 8);
}
std::vector< std::vector<cv::Point> > contours;
cv::findContours(edges, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_TC89_KCOS);
std::vector< std::vector<cv::Point> > contoursCleaned;
for (int i=0; i < contours.size(); i++) {
    if (cv::arcLength(contours[i], false) > 100)
        contoursCleaned.push_back(contours[i]);
}
std::vector<std::vector<cv::Point> > contoursArea;

for (int i=0; i < contoursCleaned.size(); i++) {
    if (cv::contourArea(contoursCleaned[i]) > 10000){
        contoursArea.push_back(contoursCleaned[i]);
    }
}
std::vector<std::vector<cv::Point> > contoursDraw (contoursCleaned.size());
for (int i=0; i < contoursArea.size(); i++){
    cv::approxPolyDP(Mat(contoursArea[i]), contoursDraw[i], 40, true);
}
Mat drawing = Mat::zeros( mat.size(), CV_8UC3 );
cv::drawContours(drawing, contoursDraw, -1, cv::Scalar(0,255,0),1);
GBF_Gabriel
źródło
Gdzie jest definicja zmiennej linii? Musi być liniami std :: vector <cv :: Vec4i>;
Can Ürek
@ CanÜrek Masz rację. std::vector<cv::Vec4i> lines;jest zadeklarowany w zakresie globalnym w moim projekcie.
GBF_Gabriel,
1
  1. Konwertuj na przestrzeń laboratoryjną

  2. Użyj klastra kmeans segment 2

  3. Następnie użyj konturów lub hough na jednym z klastrów (intenral)
user3452134
źródło