Matías, parte di questa grande squadra, ci ha fatto il favore di creare questo scritto poiché ha una perfetta padronanza dell'argomento.
Soluzione:
Ecco un potenziale approccio che utilizza operazioni morfologiche per filtrare i contorni non testuali. L'idea è:
-
Ottenere un'immagine binaria. Caricare l'immagine, in scala di grigi, quindi
Soglia di Otsu -
Rimuovere le linee orizzontali e verticali. Creare kernel orizzontali e verticali utilizzando
cv2.getStructuringElement
quindi rimuovere le linee concv2.drawContours
-
Rimuove le linee diagonali, gli oggetti circolari e i contorni curvi. Filtrare utilizzando l'area del contorno
cv2.contourArea
e l'approssimazione del contornocv2.approxPolyDP
per isolare i contorni non di testo -
Estrazione di ROI di testo e OCR. Individuare i contorni e filtrare le ROI, quindi eseguire l'OCR con
Pytesseract.
Rimozione delle linee orizzontali evidenziate in verde
Linee verticali rimosse
Rimossi contorni assortiti non di testo (linee diagonali, oggetti circolari e curve)
Rilevava le regioni di testo
import cv2
import numpy as np
import pytesseract
pytesseract.pytesseract.tesseract_cmd = r"C:Program FilesTesseract-OCRtesseract.exe"
# Load image, grayscale, Otsu's threshold
image = cv2.imread('1.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
clean = thresh.copy()
# Remove horizontal lines
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15,1))
detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
cv2.drawContours(clean, [c], -1, 0, 3)
# Remove vertical lines
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,30))
detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
cnts = cv2.findContours(detect_vertical, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
cv2.drawContours(clean, [c], -1, 0, 3)
cnts = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
# Remove diagonal lines
area = cv2.contourArea(c)
if area < 100:
cv2.drawContours(clean, [c], -1, 0, 3)
# Remove circle objects
elif area > 1000:
cv2.drawContours(clean, [c], -1, 0, -1)
# Remove curve stuff
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
x,y,w,h = cv2.boundingRect(c)
if len(approx) == 4:
cv2.rectangle(clean, (x, y), (x + w, y + h), 0, -1)
open_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
opening = cv2.morphologyEx(clean, cv2.MORPH_OPEN, open_kernel, iterations=2)
close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,2))
close = cv2.morphologyEx(opening, cv2.MORPH_CLOSE, close_kernel, iterations=4)
cnts = cv2.findContours(close, 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 = cv2.contourArea(c)
if area > 500:
ROI = image[y:y+h, x:x+w]
ROI = cv2.GaussianBlur(ROI, (3,3), 0)
data = pytesseract.image_to_string(ROI, lang='eng',config='--psm 6')
if data.isalnum():
cv2.rectangle(image, (x, y), (x + w, y + h), (36,255,12), 2)
print(data)
cv2.imwrite('image.png', image)
cv2.imwrite('clean.png', clean)
cv2.imwrite('close.png', close)
cv2.imwrite('opening.png', opening)
cv2.waitKey()
Bene, ecco un'altra possibile soluzione. So che lavorate con Python - io lavoro con il C++. Vi darò alcune idee e spero che, se lo desiderate, possiate implementare questa risposta.
L'idea principale è quella di non usare la pre-elaborazione (almeno non nella fase iniziale) e concentrarsi invece su ogni carattere di destinazione, ottenendo qualche proprietà e filtro ogni blob in base a queste proprietà.
Sto cercando di non usare la pre-elaborazione perché: 1) i filtri e le fasi morfologiche potrebbero degradare la qualità dei blob e 2) i vostri blob target sembrano presentare alcune caratteristiche che potremmo sfruttare, soprattutto: rapporto d'aspetto e area.
Guardate, i numeri e le lettere sembrano tutti più alti che larghi... inoltre, sembrano variare all'interno di un certo valore di area. Ad esempio, si desidera scartare gli oggetti "troppo larghi" oppure "troppo grande".
L'idea è di filtrare tutto ciò che non rientra nei valori precalcolati. Ho esaminato i caratteri (numeri e lettere) e ho ottenuto valori di area minima e massima e un rapporto minimo di aspetto (qui, il rapporto tra altezza e larghezza).
Lavoriamo sull'algoritmo. Si inizia leggendo l'immagine e ridimensionandola a metà delle dimensioni. L'immagine è troppo grande. Convertire in scala di grigi e ottenere un'immagine binaria tramite otsu, ecco lo pseudo-codice:
//Read input:
inputImage = imread( "diagram.png" );
//Resize Image;
resizeScale = 0.5;
inputResized = imresize( inputImage, resizeScale );
//Convert to grayscale;
inputGray = rgb2gray( inputResized );
//Get binary image via otsu:
binaryImage = imbinarize( inputGray, "Otsu" );
Bene. Lavoreremo con questa immagine. È necessario esaminare ogni blob bianco e applicare un "filtro proprietà". Sto usando componenti collegati con statistiche per eseguire un ciclo su ogni blob e ottenerne l'area e il rapporto d'aspetto; in C++ questo viene fatto come segue:
//Prepare the output matrices:
cv::Mat outputLabels, stats, centroids;
int connectivity = 8;
//Run the binary image through connected components:
int numberofComponents = cv::connectedComponentsWithStats( binaryImage, outputLabels, stats, centroids, connectivity );
//Prepare a vector of colors – color the filtered blobs in black
std::vector colors(numberofComponents+1);
colors[0] = cv::Vec3b( 0, 0, 0 ); // Element 0 is the background, which remains black.
//loop through the detected blobs:
for( int i = 1; i <= numberofComponents; i++ ) {
//get area:
auto blobArea = stats.at(i, cv::CC_STAT_AREA);
//get height, width and compute aspect ratio:
auto blobWidth = stats.at(i, cv::CC_STAT_WIDTH);
auto blobHeight = stats.at(i, cv::CC_STAT_HEIGHT);
float blobAspectRatio = (float)blobHeight/(float)blobWidth;
//Filter your blobs…
};
Ora applicheremo il filtro delle proprietà. Si tratta solo di un confronto con le soglie precalcolate. Ho usato i seguenti valori:
Minimum Area: 40 Maximum Area:400
MinimumAspectRatio: 1
All'interno del vostro for
confronta le proprietà attuali del blob con questi valori. Se i test sono positivi, si "dipinge" il blob di nero. Continuando all'interno del ciclo for
:
//Filter your blobs…
//Test the current properties against the thresholds:
bool areaTest = (blobArea > maxArea)||(blobArea < minArea);
bool aspectRatioTest = !(blobAspectRatio > minAspectRatio); //notice we are looking for TALL elements!
//Paint the blob black:
if( areaTest || aspectRatioTest ){
//filtered blobs are colored in black:
colors[i] = cv::Vec3b( 0, 0, 0 );
}else{
//unfiltered blobs are colored in white:
colors[i] = cv::Vec3b( 255, 255, 255 );
}
Dopo il ciclo, si costruisce l'immagine filtrata:
cv::Mat filteredMat = cv::Mat::zeros( binaryImage.size(), CV_8UC3 );
for( int y = 0; y < filteredMat.rows; y++ ){
for( int x = 0; x < filteredMat.cols; x++ )
{
int label = outputLabels.at(y, x);
filteredMat.at(y, x) = colors[label];
}
}
E... questo è praticamente tutto. Sono stati filtrati tutti gli elementi che non sono simili a ciò che si sta cercando. Eseguendo l'algoritmo si ottiene questo risultato:
Ho inoltre trovato le Bounding Box dei blob per visualizzare meglio i risultati:
Come si vede, alcuni elementi non vengono individuati. È possibile affinare il "filtro delle proprietà" per identificare meglio i caratteri che si stanno cercando. Una soluzione più profonda, che implica un po' di apprendimento automatico, richiede la costruzione di un "vettore di caratteristiche ideali", l'estrazione di caratteristiche dai blob e il confronto di entrambi i vettori tramite una misura di somiglianza. È anche possibile applicare alcuni post-per migliorare i risultati...
Comunque, amico, il tuo problema non è banale né facilmente scalabile, e io ti sto solo dando delle idee. Speriamo che tu riesca a implementare la tua soluzione.
Un metodo è quello di utilizzare la finestra scorrevole (è costoso).
Determinare la dimensione dei caratteri nell'immagine (tutti i caratteri sono della stessa dimensione come si vede nell'immagine) e impostare la dimensione della finestra. Provare tesseract per il rilevamento (l'immagine di input richiede una pre-elaborazione). Se una finestra rileva caratteri consecutivi, memorizzare le coordinate della finestra. Unire le coordinate e ottenere la regione dei caratteri.
Commenti e valutazioni
Ricorda che ti diamo la possibilità di dire se hai risposto alla tua domanda appena in tempo.