Reinigungsbild für OCR

9

Ich habe versucht, Bilder für OCR zu löschen: (die Linien)

Geben Sie hier die Bildbeschreibung ein

Ich muss diese Zeilen entfernen, um das Bild manchmal weiter zu verarbeiten, und ich komme mir ziemlich nahe, aber die meiste Zeit nimmt der Schwellenwert zu viel vom Text weg:

    copy = img.copy()
    blur = cv2.GaussianBlur(copy, (9,9), 0)
    thresh = cv2.adaptiveThreshold(blur,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV,11,30)

    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9,9))
    dilate = cv2.dilate(thresh, kernel, iterations=2)

    cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if len(cnts) == 2 else cnts[1]

    for c in cnts:
        area = cv2.contourArea(c)
        if area > 300:
            x,y,w,h = cv2.boundingRect(c)
            cv2.rectangle(copy, (x, y), (x + w, y + h), (36,255,12), 3)

Bearbeiten: Außerdem funktioniert die Verwendung konstanter Zahlen nicht, falls sich die Schriftart ändert. Gibt es eine generische Möglichkeit, dies zu tun?

K41F4r
quelle
2
Einige dieser Zeilen oder Fragmente davon haben dieselben Eigenschaften wie Rechtstext, und es wird schwierig sein, sie zu entfernen, ohne gültigen Text zu verderben. In diesem Fall können Sie sich auf die Fakten konzentrieren, dass sie länger als Zeichen und etwas isoliert sind. Ein erster Schritt könnte also darin bestehen, die Größe und Nähe der Zeichen abzuschätzen.
Yves Daoust
@YvesDaoust Wie würde man vorgehen, um die Nähe von Charakteren zu finden? (da das Filtern nur nach Größe
oft
1
Sie konnten für jeden Blob die Entfernung zum nächsten Nachbarn finden. Dann würden Sie durch Histogrammanalyse der Entfernungen einen Schwellenwert zwischen "nah" und "auseinander" (so etwas wie die Art der Verteilung) oder zwischen "umgeben" und "isoliert" finden.
Yves Daoust
Bei mehreren kleinen Linien in der Nähe wäre der nächste Nachbar nicht die andere kleine Linie? Wäre die Berechnung der durchschnittlichen Entfernung zu allen anderen Blobs zu kostspielig?
K41F4r
"Wäre ihr nächster Nachbar nicht die andere kleine Linie?": Guter Einwand, Euer Ehren. Tatsächlich unterscheiden sich einige enge kurze Segmente nicht vom legitimen Text, wenn auch in einer völlig unwahrscheinlichen Anordnung. Möglicherweise müssen Sie die Fragmente unterbrochener Linien neu gruppieren. Ich bin mir nicht sicher, ob die durchschnittliche Entfernung zu allen Sie retten würde.
Yves Daoust

Antworten:

14

Hier ist eine Idee. Wir unterteilen dieses Problem in mehrere Schritte:

  1. Bestimmen Sie die durchschnittliche rechteckige Konturfläche. Wir schwellen dann Konturen und filtern anhand des begrenzenden Rechteckbereichs der Kontur. Der Grund, warum wir dies tun, ist die Beobachtung, dass jedes typische Zeichen nur so groß ist, während großes Rauschen einen größeren rechteckigen Bereich überspannt. Wir bestimmen dann die durchschnittliche Fläche.

  2. Entfernen Sie große Ausreißerkonturen. Wir durchlaufen die Konturen erneut und entfernen die großen Konturen, wenn sie 5xgrößer als die durchschnittliche Konturfläche sind, indem wir die Kontur ausfüllen. Anstatt einen festen Schwellenwertbereich zu verwenden, verwenden wir diesen dynamischen Schwellenwert für mehr Robustheit.

  3. Erweitern Sie mit einem vertikalen Kernel, um Zeichen zu verbinden . Die Idee ist, die Beobachtung zu nutzen, dass Zeichen in Spalten ausgerichtet sind. Durch die Erweiterung mit einem vertikalen Kernel verbinden wir den Text miteinander, sodass in dieser kombinierten Kontur kein Rauschen enthalten ist.

  4. Entfernen Sie kleine Geräusche . Nachdem der zu behaltende Text verbunden ist, finden wir Konturen und entfernen alle Konturen, die kleiner als 4xdie durchschnittliche Konturfläche sind.

  5. Bitweise - und um das Bild zu rekonstruieren . Da wir nur gewünschte Konturen haben, um auf unserer Maske zu bleiben, arbeiten wir bitweise - und um den Text zu erhalten und unser Ergebnis zu erhalten.


Hier ist eine Visualisierung des Prozesses:

Wir Otsus Schwelle , um ein Binärbild zu erhalten, finden dann Konturen , um die durchschnittliche rechteckige Konturfläche zu bestimmen. Von hier aus entfernen wir die großen grün hervorgehobenen Ausreißerkonturen durch Füllen von Konturen

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Als nächstes konstruieren wir einen vertikalen Kernel und erweitern ihn , um die Charaktere zu verbinden. Dieser Schritt verbindet den gesamten gewünschten Text, um das Rauschen beizubehalten, und isoliert es in einzelne Blobs.

Geben Sie hier die Bildbeschreibung ein

Jetzt finden wir Konturen und filtern mithilfe des Konturbereichs , um das kleine Rauschen zu entfernen

Geben Sie hier die Bildbeschreibung ein

Hier sind alle entfernten Rauschpartikel grün hervorgehoben

Geben Sie hier die Bildbeschreibung ein

Ergebnis

Geben Sie hier die Bildbeschreibung ein

Code

import cv2

# Load image, grayscale, and Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Determine average contour area
average_area = [] 
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    area = w * h
    average_area.append(area)

average = sum(average_area) / len(average_area)

# Remove large lines if contour area is 5x bigger then average contour area
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    area = w * h
    if area > average * 5:  
        cv2.drawContours(thresh, [c], -1, (0,0,0), -1)

# Dilate with vertical kernel to connect characters
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,5))
dilate = cv2.dilate(thresh, kernel, iterations=3)

# Remove small noise if contour area is smaller than 4x average
cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    area = cv2.contourArea(c)
    if area < average * 4:
        cv2.drawContours(dilate, [c], -1, (0,0,0), -1)

# Bitwise mask with input image
result = cv2.bitwise_and(image, image, mask=dilate)
result[dilate==0] = (255,255,255)

cv2.imshow('result', result)
cv2.imshow('dilate', dilate)
cv2.imshow('thresh', thresh)
cv2.waitKey()

Hinweis: Die herkömmliche Bildverarbeitung beschränkt sich auf Schwellenwerte, morphologische Operationen und Konturfilterung (Konturnäherung, Fläche, Seitenverhältnis oder Blob-Erkennung). Da die Eingabebilder je nach Größe des Zeichentextes variieren können, ist es ziemlich schwierig, eine singuläre Lösung zu finden. Vielleicht möchten Sie Ihren eigenen Klassifikator mit maschinellem / tiefem Lernen für eine dynamische Lösung trainieren.

nathancy
quelle
1
Bei einer größeren Schriftart würde dies nicht auch Text löschen?
K41F4r
Ja, möglicherweise müssen Sie den Schwellenwert anpassen. Für einen dynamischeren Ansatz besteht die Idee darin, den durchschnittlichen Zeichenbereich zu bestimmen und diesen dann als Schwellenwert zu verwenden
Nathancy
Scheint zu spezifisch für das Beispiel zu sein, wenn der durchschnittliche Bereich verwendet wird, wird der Text immer noch häufig gelöscht, was das Ergebnis für OCR
K41F4r
Haben Sie ein weiteres Beispiel für ein Eingabebild, das Sie dem Beitrag hinzufügen könnten?
Nathancy
1
Es ist ziemlich schwierig, eine Lösung zu finden, die in allen Situationen mit herkömmlichen Bildverarbeitungstechniken funktioniert. Vielleicht möchten Sie Ihren eigenen Klassifikator mithilfe von Deep Learning trainieren. Viel Glück!
Nathancy