Konturları Python, OpenCV'deki önceliğe göre sıralama

Aug 26 2020

Kontürleri gelişlerine göre sıralamaya çalışıyorum left-to-rightve top-to-bottomtıpkı herhangi bir şey yazdığınız gibi. Dan, topve leftona göre hangisi.

Şimdiye kadar elde ettiğim şey bu:

def get_contour_precedence(contour, cols):
    tolerance_factor = 61
    origin = cv2.boundingRect(contour)
    return ((origin[1] // tolerance_factor) * tolerance_factor) * cols + origin[0]


image = cv2.imread("C:/Users/XXXX/PycharmProjects/OCR/raw_dataset/23.png", 0)

ret, thresh1 = cv2.threshold(image, 130, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

contours, h = cv2.findContours(thresh1.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# perform edge detection, find contours in the edge map, and sort the
# resulting contours from left-to-right
contours.sort(key=lambda x: get_contour_precedence(x, thresh1.shape[1]))

# initialize the list of contour bounding boxes and associated
# characters that we'll be OCR'ing
chars = []
inc = 0
# loop over the contours
for c in contours:
    inc += 1

    # compute the bounding box of the contour
    (x, y, w, h) = cv2.boundingRect(c)

    label = str(inc)
    cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
    cv2.putText(image, label, (x - 2, y - 2),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    print('x=', x)
    print('y=', y)
    print('x+w=', x + w)
    print('y+h=', y + h)
    crop_img = image[y + 2:y + h - 1, x + 2:x + w - 1]
    name = os.path.join("bounding boxes", 'Image_%d.png' % (
        inc))
    cv2.imshow("cropped", crop_img)
    print(name)
    crop_img = Image.fromarray(crop_img)
    crop_img.save(name)
    cv2.waitKey(0)

cv2.imshow('mat', image)
cv2.waitKey(0)

Giriş Resmi:

Çıktı Görüntüsü 1:

Giriş Resmi 2:

Resim 2 Çıktısı:

Giriş Resmi 3:

Çıktı Resmi 3:

Gördüğünüz gibi 1,2,3,4, Resim Numarası 3'te gösterildiği gibi her bir resim olmasını beklediğim gibi değil.

Bunu çalışmasını sağlamak veya hatta özel bir işlev yazmak için nasıl ayarlayabilirim?

NOT: Sorumda verilen aynı giriş görüntüsünün birden çok resmine sahibim. İçerik aynıdır, ancak metinde varyasyonları vardır, bu nedenle tolerance factorher biri için çalışmıyor. Manuel olarak ayarlamak iyi bir fikir olmaz.

Yanıtlar

1 eldesgraciado Aug 31 2020 at 08:39

Bu benim sorunla ilgili düşüncem. Size genel özünü vereceğim ve ardından uygulamamı C++. Ana fikir, resmi soldan sağa , yukarıdan aşağıya işlemek istememdir . Her blobu (veya konturu) bulduğum gibi işleyeceğim , ancak başarılı (sıralı) bir segmentasyon elde etmek için birkaç ara adıma ihtiyacım var.

Satırları kullanarak dikey sıralama

İlk adım lekeler sıralamak için çalışıyor satırlara göre her bir satır (sırasız) 'in bir grubu vardır, bu araçlar - yatay lekeler. Bu iyi. ilk adım, bir tür dikey sıralama hesaplamaktır ve her satırı yukarıdan aşağıya doğru işlersek, tam da bunu başarırız.

Bloblar (dikey olarak) satırlara göre sıralandıktan sonra, ağırlık merkezlerini (veya kütle merkezlerini) kontrol edebilir ve yatay olarak sıralayabilirim. Buradaki fikir, satır başına satır işleyeceğim ve forher satırda blob centroidleri sıralayacağım . Burada başarmaya çalıştığım şeyin bir örneğini görelim.

Bu sizin giriş resminiz:

Ben buna Satır Maskesi diyorum :

Bu son görüntü, her biri bir "satırı" temsil eden beyaz alanlar içerir . Her satır (örneğin, bir numarası vardır Row1, Row2vs.) ve her row(bu durumda veya karakter) lekeler bir dizi tutar. Her işleyerek row, alt gelen üst , zaten dikey eksende blob'ları tasnif edilmektedir.

Her satırı yukarıdan aşağıya numaralandırırsam, şu resmi elde ederim:

Satır Maskesi "lekeler satırları" yaratma yoludur ve bu maske hesaplanabilir morfolojik . İşleme sırasını daha iyi görebilmeniz için üst üste yerleştirilmiş 2 resme göz atın:

Burada yapmaya çalıştığımız şey, önce dikey bir sıralama (mavi ok) ve sonra yatay (kırmızı ok) sıralamayı halledeceğiz. Her satırı işleyerek sıralama problemini (muhtemelen) aşabileceğimizi görebilirsiniz!

Ağırlık merkezlerini kullanarak yatay sıralama

Şimdi lekeleri nasıl sıralayabileceğimize bakalım horizontally. Biz birlikte, daha basit bir görüntü oluşturursanız widthgiriş görüntüsünün eşit ve heightsayıları eşit rowsbizim de Satır Maske , biz sadece her damla sentroidin (x koordinatı) her koordinat yatay bindirebilirsiniz. Bu örneğe bakın:

Bu bir Satır Tablosudur . Her satır, Satır Maskesinde bulunan satır sayısını temsil eder ve ayrıca yukarıdan aşağıya doğru okunur. widthTablonun aynıdır widthiçin giriş görüntüsünün ve yatay eksene göre uzamsal olarak karşılık gelir . Her bir kare , girdi görüntünüzdeki bir pikseldir ve yalnızca yatay koordinat kullanılarak Satır Tablosuna eşlenir (çünkü satırları basitleştirmemiz oldukça basittir). Satır tablosundaki her pikselin gerçek değeri, labelgiriş görüntünüzdeki her bir blob'u etiketleyen a'dır. Etiketlerin sipariş edilmediğini unutmayın!

Örneğin, bu tablo, 1. satırda ( 1. satırın ne olduğunu zaten biliyorsunuz - bu, Satır Maskesindeki ilk beyaz alan ) (1,4), blob numarasının bulunduğu konumda olduğunu gösterir 3. Pozisyonda (1,6)damla numara var 2böyle devam, vb. Bu tablonun harika yanı (bence), üzerinde döngü yapabilmeniz ve yatay sıralamadan forfarklı olan her değer 0çok önemsiz hale geliyor. Bu, şimdi soldan sağa sıralanan satır tablosudur:

Ağırlık merkezleriyle blob bilgilerini eşleme

Biz lekeler kullanacağız sentroidler için mapbizim iki gösterim (Sıra Maske / Satır Tablo) arasındaki bilgi. Zaten her iki "yardımcı" görüntünüzün olduğunu ve bir seferde giriş görüntüsündeki her blobu (veya konturu) işlediğinizi varsayalım. Örneğin, başlangıç ​​olarak bu var:

Pekala, burada bir damla var. Bunu Satır Maskesine ve Satır Tablosuna nasıl eşleyebiliriz ? Onun kullanma sentroidler . Ağırlık merkezini (şekilde yeşil nokta olarak gösterilen) hesaplarsak, bir ağırlık dictionarymerkezi ve etiket oluşturabiliriz. Örneğin, bu blob için centroid, adresinde bulunur (271,193). Tamam, atayalım label = 1. Şimdi bu sözlüğe sahibiz:

Şimdi, rowbu blobun aynı centroidşekilde Row Mask üzerinde yerleştirildiğini buluyoruz . Bunun gibi bir şey:

rowNumber = rowMask.at( 271,193 )

Bu operasyon geri dönmelidir rownNumber = 3. Güzel! Blobumuzun hangi sıraya yerleştirildiğini biliyoruz ve bu nedenle artık dikey olarak sıralanmıştır. Şimdi yatay koordinatını Satır Tablosunda saklayalım:

rowTable.at( 271, 193 ) = 1

Şimdi, rowTableişlenen blobun etiketini (satırında ve sütununda) tutar. Satır Tablosu şunun gibi görünmelidir:

Tablo çok daha geniştir , çünkü yatay boyutu girdi görüntünüzle aynı olmak zorundadır. Bu görüntüde, yerine label 1yerleştirilir Bu görüntünüzdeki Column 271, Row 3.tek blob olsaydı, lekeler zaten sıralanırdı. Eğer, diyelim ki, başka damla eklersek ne olur Column 2, Row 1? Bu nedenle, tüm blobları işledikten sonra, etiketlerini doğru şekilde düzeltmek için bu tabloyu tekrar geçmeniz gerekir.

C ++ ile uygulama

Pekala, umarım algoritma biraz açık olmalıdır (eğer değilse, sor, dostum). Bu fikirleri OpenCVkullanarak uygulamaya çalışacağım C++. Öncelikle, görüşlerinizden binary imagebirine ihtiyacım var . Hesaplama Otsu’s thresholdingyöntemi kullanarak önemsizdir :

//Read the input image:
std::string imageName = "C://opencvImages//yFX3M.png";
cv::Mat testImage = cv::imread( imageName );

//Compute grayscale image
cv::Mat grayImage;
cv::cvtColor( testImage, grayImage, cv::COLOR_RGB2GRAY );

//Get binary image via Otsu:
cv::Mat binImage;
cv::threshold( grayImage, binImage, 0, 255, cv::THRESH_OTSU );

//Invert image:
binImage = 255 - binImage;

Bu sonuçta ortaya çıkan ikili görüntüdür, hiçbir şey fantezi değil, tam da çalışmaya başlamak için ihtiyacımız olan şey:

İlk adım Row Mask,. Bu, morfoloji kullanılarak elde edilebilir. Sadece ÇOK büyük bir yatay dilation + erosionile a uygulayın . Buradaki fikir, bu lekeleri dikdörtgenlere dönüştürmek, onları yatay olarak "birleştirmek":structuring element

//Create a hard copy of the binary mask:
cv::Mat rowMask = binImage.clone();

//horizontal dilation + erosion:
int horizontalSize = 100; // a very big horizontal structuring element
cv::Mat SE = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(horizontalSize,1) );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_DILATE, SE, cv::Point(-1,-1), 2 );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_ERODE, SE, cv::Point(-1,-1), 1 );

Bu, aşağıdakilerle sonuçlanır Row Mask:

Bu çok havalı, şimdi bizde olduğuna göre Row Mask, onları sıraları numaralandırmalıyız, tamam mı? Bunu yapmanın birçok yolu var, ancak şu anda daha basit olanla ilgileniyorum: bu görüntüde döngü yapın ve her bir pikseli elde edin. Ifbir piksel beyazsa, Flood Fillgörüntünün bu bölümünü benzersiz bir blob (veya bu durumda satır) olarak etiketlemek için bir işlem kullanın. Bu şöyle yapılabilir:

//Label the row mask:
int rowCount = 0; //This will count our rows

//Loop thru the mask:
for( int y = 0; y < rowMask.rows; y++ ){
    for( int x = 0; x < rowMask.cols; x++ ){
        //Get the current pixel:
        uchar currentPixel = rowMask.at<uchar>( y, x );
        //If the pixel is white, this is an unlabeled blob:
        if ( currentPixel == 255 ) {
            //Create new label (different from zero):
            rowCount++;
            //Flood fill on this point:
            cv::floodFill( rowMask, cv::Point( x, y ), rowCount, (cv::Rect*)0, cv::Scalar(), 0 );
        }
    }
}

Bu süreç tüm satırları etiketleyecektir 1için r. İstediğimiz buydu. Görüntüye bakarsanız, satırları soluk bir şekilde görürsünüz, çünkü etiketlerimiz gri tonlamalı piksellerin çok düşük yoğunluk değerlerine karşılık gelir.

Tamam, şimdi Satır Tablosunu hazırlayalım . Bu "tablo" gerçekten başka bir resim, unutmayın: girişle aynı genişlik ve üzerinde saydığınız satır sayısıyla yükseklik Row Mask:

//create rows image:
cv::Mat rowTable = cv::Mat::zeros( cv::Size(binImage.cols, rowCount), CV_8UC1 );
//Just for convenience:
rowTable = 255 - rowTable;

Burada, kolaylık sağlamak için son görüntüyü tersine çevirdim. Çünkü tablonun (çok düşük yoğunluklu) piksellerle nasıl doldurulduğunu gerçekten görmek ve her şeyin amaçlandığı gibi çalıştığından emin olmak istiyorum.

Şimdi eğlenceli kısım geliyor. Hazırladığımız iki görselimiz (veya veri taşıyıcıları) var. Her blobu bağımsız olarak işlememiz gerekir. Buradaki fikir, ikili görüntüden her bir blob / kontur / karakteri çıkarmanız ve bunu hesaplamanız centroidve yeni bir label. Yine, bunu yapmanın birçok yolu var. Burada aşağıdaki yaklaşımı kullanıyorum:

Döneceğim binary mask. current biggest blobBu ikili girişten alacağım . Onu hesaplayacağım centroidve verilerini gereken her kapta depolayacağım ve sonra deletemaskeden o blob'u edeceğim . Daha fazla damla kalmayana kadar işlemi tekrarlayacağım. Bunu yapma şeklim bu, özellikle bunun için zaten yazdığım işlevlerim olduğu için. Yaklaşım şu:

//Prepare a couple of dictionaries for data storing:
std::map< int, cv::Point > blobMap; //holds label, gives centroid
std::map< int, cv::Rect > boundingBoxMap; //holds label, gives bounding box

Birincisi, iki dictionaries. Biri bir damla etiketi alır ve ağırlık merkezini döndürür. Diğeri aynı etiketi alır ve sınırlayıcı kutuyu döndürür.

//Extract each individual blob:
cv::Mat bobFilterInput = binImage.clone();

//The new blob label:
int blobLabel = 0;

//Some control variables:
bool extractBlobs = true; //Controls loop
int currentBlob = 0; //Counter of blobs

while ( extractBlobs ){

    //Get the biggest blob:
    cv::Mat biggestBlob = findBiggestBlob( bobFilterInput );

    //Compute the centroid/center of mass:
    cv::Moments momentStructure = cv::moments( biggestBlob, true );
    float cx = momentStructure.m10 / momentStructure.m00;
    float cy = momentStructure.m01 / momentStructure.m00;

    //Centroid point:
    cv::Point blobCentroid;
    blobCentroid.x = cx;
    blobCentroid.y = cy;

    //Compute bounding box:
    boundingBox boxData;
    computeBoundingBox( biggestBlob, boxData );

    //Convert boundingBox data into opencv rect data:
    cv::Rect cropBox = boundingBox2Rect( boxData );


    //Label blob:
    blobLabel++;
    blobMap.emplace( blobLabel, blobCentroid );
    boundingBoxMap.emplace( blobLabel, cropBox );

    //Get the row for this centroid
    int blobRow = rowMask.at<uchar>( cy, cx );
    blobRow--;

    //Place centroid on rowed image:
    rowTable.at<uchar>( blobRow, cx ) = blobLabel;

    //Resume blob flow control:
    cv::Mat blobDifference = bobFilterInput - biggestBlob;
    //How many pixels are left on the new mask?
    int pixelsLeft = cv::countNonZero( blobDifference );
    bobFilterInput = blobDifference;

    //Done extracting blobs?
    if ( pixelsLeft <= 0 ){
        extractBlobs = false;
    }

    //Increment blob counter:
    currentBlob++;

}

Bu işlemenin her blobun nasıl geçtiğini, işlediğini ve hiçbir şey kalmayana kadar sildiğini gösteren güzel bir animasyona bakın:

Şimdi, yukarıdaki pasajla bazı notlar. Bazı yardımcı işlevlerim var: en büyükBlob ve computeBoundingBox. Bu işlevler, ikili bir görüntüdeki en büyük blobu hesaplar ve bir sınırlayıcı kutunun özel bir yapısını sırasıyla OpenCV'ın Rectyapısına dönüştürür . Bunlar, işlevlerin gerçekleştirdiği işlemlerdir.

Snippet'in "et" şudur: Bir edindikten sonra izole blob , onun hesaplamak centroid(aslında hesaplamak center of massyoluyla central moments). Yeni bir label. Bu saklayın labelve centroidbir de dictionarybenim durumumda, içinde, blobMapsözlüğe. Ayrıca hesaplamak bounding boxve başka depolar dictionary, boundingBoxMap:

//Label blob:
blobLabel++;
blobMap.emplace( blobLabel, blobCentroid );
boundingBoxMap.emplace( blobLabel, cropBox );

Şimdi, kullanarak centroidverileri fetchkarşılık gelen rowbu blob'un. Satırı aldıktan sonra, bu numarayı satır tablonuza kaydedin:

//Get the row for this centroid
int blobRow = rowMask.at<uchar>( cy, cx );
blobRow--;

//Place centroid on rowed image:
rowTable.at<uchar>( blobRow, cx ) = blobLabel;

Mükemmel. Bu noktada Satır Tablosu hazır olur. Dönüp dolaşalım ve aslında ve son olarak şu lanet olası lekeleri sipariş edelim:

int blobCounter = 1; //The ORDERED label, starting at 1
for( int y = 0; y < rowTable.rows; y++ ){
    for( int x = 0; x < rowTable.cols; x++ ){
        //Get current label:
        uchar currentLabel = rowTable.at<uchar>( y, x );
        //Is it a valid label?
        if ( currentLabel != 255 ){
            //Get the bounding box for this label:
            cv::Rect currentBoundingBox = boundingBoxMap[ currentLabel ];
            cv::rectangle( testImage, currentBoundingBox, cv::Scalar(0,255,0), 2, 8, 0 );
            //The blob counter to string:
            std::string counterString = std::to_string( blobCounter );
            cv::putText( testImage, counterString, cv::Point( currentBoundingBox.x, currentBoundingBox.y-1 ),
                         cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255,0,0), 1, cv::LINE_8, false );
            blobCounter++; //Increment the blob/label
        }
    }
}

Süslü bir şey yok, sadece normal bir iç içe fordöngü row table,. Piksel beyaz farklı ise, kullanmak labelalmak hem centroidve bounding boxve sadece değiştirmek labelartan sayıda. Sonuç görüntüleme için sınırlayıcı kutuları ve yeni etiketi orijinal görüntünün üzerine çiziyorum.

Bu animasyonda sıralı işlemeye göz atın:

Çok güzel, işte bonus bir animasyon, Satır Tablosu yatay koordinatlarla dolduruluyor:

2 antoine Aug 26 2020 at 19:21

Konturun sol üst köşesini almak yerine, ağırlık merkezini veya en azından sınırlayıcı kutu merkezini kullanmayı tercih ederim.

def get_contour_precedence(contour, cols):
tolerance_factor = 4
origin = cv2.boundingRect(contour)
return (((origin[1] + origin[3])/2 // tolerance_factor) * tolerance_factor) * cols + (origin[0] + origin[2]) / 2

Ancak her durumda işe yarayan bir tolerans değeri bulmak zor olabilir.

2 t2solve Aug 30 2020 at 21:33

Hatta, bir çokgenin merkez noktası için dikdörtgenin "normal" koordinat merkez noktasından daha iyi bir tahmin olma eğiliminde olan renk anları kullanmayı bile söyleyebilirim, böylece fonksiyon şöyle olabilir:

def get_contour_precedence(contour, cols):
     tolerance_factor = 61
     M = cv2.moments(contour)
     # calculate x,y coordinate of centroid
     if M["m00"] != 0:
             cX = int(M["m10"] / M["m00"])
             cY = int(M["m01"] / M["m00"])
     else:
     # set values as what you need in the situation
             cX, cY = 0, 0
     return ((cY // tolerance_factor) * tolerance_factor) * cols + cX

süper bir matematik. ton anlarının ne olduğuna dair açıklama, burada bulabilir misin

Belki merkezinizi satırlara ve sütunlara ayırmak için genel olarak kmeans gibi bir kümeleme algoritması kullanarak bu tolerans faktöründen kurtulmayı düşünmelisiniz. OpenCv, burada bulabileceğiniz bir kmeans uygulamasına sahiptir.

Hedefinizin tam olarak ne olduğunu bilmiyorum, ancak başka bir fikir , her satırı daha fazla işleme için bir İlgi Bölgesine (ROI) ayırmak olabilir, daha sonra harfleri her konturun ve çizginin X Değerlerine göre kolayca sayabilirsiniz. numara

import cv2
import numpy as np

## (1) read
img = cv2.imread("yFX3M.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## (2) threshold
th, threshed = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)

## (3) minAreaRect on the nozeros
pts = cv2.findNonZero(threshed)
ret = cv2.minAreaRect(pts)

(cx,cy), (w,h), ang = ret
if w>h:
    w,h = h,w

## (4) Find rotated matrix, do rotation
M = cv2.getRotationMatrix2D((cx,cy), ang, 1.0)
rotated = cv2.warpAffine(threshed, M, (img.shape[1], img.shape[0]))

## (5) find and draw the upper and lower boundary of each lines
hist = cv2.reduce(rotated,1, cv2.REDUCE_AVG).reshape(-1)

th = 2
H,W = img.shape[:2]
#   (6) using histogramm with threshold
uppers = [y for y in range(H-1) if hist[y]<=th and hist[y+1]>th]
lowers = [y for y in range(H-1) if hist[y]>th and hist[y+1]<=th]

rotated = cv2.cvtColor(rotated, cv2.COLOR_GRAY2BGR)
for y in uppers:
    cv2.line(rotated, (0,y), (W, y), (255,0,0), 1)

for y in lowers:
    cv2.line(rotated, (0,y), (W, y), (0,255,0), 1)
cv2.imshow('pic', rotated)

# (7) we iterate all rois and count 
for i in range(len(uppers)) : 
    print('line=',i)
    roi = rotated[uppers[i]:lowers[i],0:W]
    cv2.imshow('line', roi)
    cv2.waitKey(0)
    # here again calc thres and contours

Burada bu kodla eski bir gönderi buldum

1 fmw42 Aug 31 2020 at 04:53

İşte Python / OpenCV'de önce satırlara sonra karakterlere göre işlemenin bir yolu.

  • Girişi oku
  • Gri tonlamaya dönüştür
  • Eşik ve ters çevir
  • Uzun yatay çekirdekler kullanın ve satırlara yakın morfoloji uygulayın
  • Satırların ve sınırlayıcı kutularının dış hatlarını alın
  • Satır kutularını kaydedin ve Y'ye göre sıralayın
  • Sıralanan her satır kutusunun üzerinde döngü yapın ve eşikli görüntüden satırı çıkarın
  • Satırdaki her karakterin dış hatlarını alın ve karakterlerin sınırlayıcı kutularını kaydedin.
  • X'te belirli bir satır için konturları sıralayın
  • Girişteki sınırlayıcı kutuları ve indeks numarasını görüntü üzerine metin olarak çizin
  • Dizini artırın
  • Sonuçları kaydedin

Giriş:

import cv2
import numpy as np

# read input image
img = cv2.imread('vision78.png')

# convert img to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# otsu threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU )[1]
thresh = 255 - thresh 

# apply morphology close to form rows
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (51,1))
morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)

# find contours and bounding boxes of rows
rows_img = img.copy()
boxes_img = img.copy()
rowboxes = []
rowcontours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
rowcontours = rowcontours[0] if len(rowcontours) == 2 else rowcontours[1]
index = 1
for rowcntr in rowcontours:
    xr,yr,wr,hr = cv2.boundingRect(rowcntr)
    cv2.rectangle(rows_img, (xr, yr), (xr+wr, yr+hr), (0, 0, 255), 1)
    rowboxes.append((xr,yr,wr,hr))

# sort rowboxes on y coordinate
def takeSecond(elem):
    return elem[1]
rowboxes.sort(key=takeSecond)
    
# loop over each row    
for rowbox in rowboxes:
    # crop the image for a given row
    xr = rowbox[0]
    yr = rowbox[1]
    wr = rowbox[2]
    hr = rowbox[3]  
    row = thresh[yr:yr+hr, xr:xr+wr]
    bboxes = []
    # find contours of each character in the row
    contours = cv2.findContours(row, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours = contours[0] if len(contours) == 2 else contours[1]
    for cntr in contours:
        x,y,w,h = cv2.boundingRect(cntr)
        bboxes.append((x+xr,y+yr,w,h))
    # sort bboxes on x coordinate
    def takeFirst(elem):
        return elem[0]
    bboxes.sort(key=takeFirst)
    # draw sorted boxes
    for box in bboxes:
        xb = box[0]
        yb = box[1]
        wb = box[2]
        hb = box[3]
        cv2.rectangle(boxes_img, (xb, yb), (xb+wb, yb+hb), (0, 0, 255), 1)
        cv2.putText(boxes_img, str(index), (xb,yb), cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.75, (0,255,0), 1)
        index = index + 1
    
# save result
cv2.imwrite("vision78_thresh.jpg", thresh)
cv2.imwrite("vision78_morph.jpg", morph)
cv2.imwrite("vision78_rows.jpg", rows_img)
cv2.imwrite("vision78_boxes.jpg", boxes_img)

# show images
cv2.imshow("thresh", thresh)
cv2.imshow("morph", morph)
cv2.imshow("rows_img", rows_img)
cv2.imshow("boxes_img", boxes_img)
cv2.waitKey(0)

Eşik görüntüsü:

Satırların morfoloji görüntüsü:

Satır konturları görüntüsü:

Karakter konturları görüntüsü: