Asosiy qismga o'tish

Threshold va morfologiya — hujjatni tozalash

3-bob 13 kod bloki 1405 so'z

Skanerlangan hujjat hech qachon toza bo'lmaydi: soya, shovqin, notekis yorug'lik. OCR ga berishdan oldin tozalash kerak.

Threshold — oq va qora

G'oya oddiy: chegaradan qorong'i piksel → qora, yorug' → oq.

python5 qator
import cv2

kulrang = cv2.imread("hujjat.png", cv2.IMREAD_GRAYSCALE)
_, oq_qora = cv2.threshold(kulrang, 128, 255, cv2.THRESH_BINARY)
cv2.imwrite("oq_qora.png", oq_qora)

Muammo: bitta chegara YETMAYDI

Slayderni suring — chap tomon yorug', o'ng tomon soyada:

Ko'rdingizmi? Bitta chegara ikkala tomonga to'g'ri kelmaydi. Chegarani pasaytirsangiz — o'ngdagi matn yo'qoladi. Oshirsangiz — soya ham qora bo'ladi.

Bu haqiqiy muammo: telefonda suratga olingan hujjatda doim notekis yorug'lik bo'ladi.

Yechim 1: Otsu — chegarani AVTOMATIK topish

python3 qator
chegara, natija = cv2.threshold(
    kulrang, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsu topgan chegara: {chegara}")

Bizning sinov rasmida 0.0 chiqadi. Sabab: hujjat.png sof oq (255) va sof qora (0) dan iborat — oraliq qiymatlar yo'q. Bunday rasmda "eng yaxshi chegara" tushunchasi ma'nosiz.

Haqiqiy skanerlangan hujjatda 100–180 oralig'ida son chiqadi. Sinovni haqiqiy surat bilan takrorlang.

Otsu gistogrammaga qaraydi va qora/oq guruhlarni eng yaxshi ajratadigan chegarani topadi. Qo'lda tanlashdan yaxshiroq.

Lekin u ham BITTA chegara beradi — notekis yorug'lik muammosini hal qilmaydi.

Yechim 2: Adaptive threshold — HAR HUDUD uchun alohida

python7 qator
adaptiv = cv2.adaptiveThreshold(
    kulrang, 255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,   # qo'shnilarning vaznli o'rtachasi
    cv2.THRESH_BINARY,
    blockSize=31,                      # hudud o'lchami (TOQ son)
    C=10,                              # o'rtachadan ayiriladigan qiymat
)

Bu har bir piksel uchun atrofidagi 31x31 hududga qarab chegara hisoblaydi. Soyadagi hudud uchun past chegara, yorug' hudud uchun baland.

blockSize toq bo'lishi SHART — markaz piksel kerak. Juft son bersangiz xato chiqadi.

Qanday tanlash:

  • blockSize — matn balandligidan kattaroq (odatda 15–51)
  • C — kattalashtirilsa natija oqroq, shovqin kamayadi

Morfologiya — shakl bilan ishlash

Threshold dan keyin mayda nuqtalar (shovqin) qoladi. Yoki harflar uzilib qoladi. Morfologiya shuni tuzatadi.

Ikki asosiy amal:

python6 qator
import numpy as np

yadro = np.ones((3, 3), np.uint8)

eroziya  = cv2.erode(oq_qora, yadro, iterations=1)   # qora KICHRAYADI
dilatsiya = cv2.dilate(oq_qora, yadro, iterations=1)  # qora KATTALASHADI

Eroziya — obyektni yupqalashtiradi, mayda nuqtalarni yo'qotadi. Dilatsiya — obyektni qalinlashtiradi, uzilgan joyni ulaydi.

Ikkalasini ketma-ket qo'llash foydali:

python5 qator
# OPENING = eroziya -> dilatsiya : SHOVQINNI o'chiradi
tozalangan = cv2.morphologyEx(oq_qora, cv2.MORPH_OPEN, yadro)

# CLOSING = dilatsiya -> eroziya : TESHIKLARNI to'ldiradi
butun = cv2.morphologyEx(oq_qora, cv2.MORPH_CLOSE, yadro)

Opening — mayda oq nuqtalar yo'qoladi, katta shakllar qoladi. Closing — harf ichidagi uzilishlar to'ladi.

Hujjat uchun odatiy tartib: adaptive threshold → opening (shovqin ketadi) → agar harflar uzilgan bo'lsa closing.

Kontur — shakllarni topish

Kontur — bu bir xil rangdagi hududning chegarasi.

python4 qator
konturlar, _ = cv2.findContours(
    oq_qora, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

print(f"topilgan kontur: {len(konturlar)}")

DIQQAT: matn OQ bo'lishi kerak

findContours oq hududlarni obyekt deb biladi, qorani fon deb.

Bizning hujjatda esa matn qora, fon oq — teskari. Shuning uchun oddiy THRESH_BINARY bilan quyidagi natija chiqadi:

python7 qator
_, oddiy = cv2.threshold(kulrang, 128, 255, cv2.THRESH_BINARY)
k1, _ = cv2.findContours(oddiy, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"BINARY     -> {len(k1)} kontur")

_, teskari = cv2.threshold(kulrang, 128, 255, cv2.THRESH_BINARY_INV)
k2, _ = cv2.findContours(teskari, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"BINARY_INV -> {len(k2)} kontur")

Natija:

matn2 qator
BINARY     -> 1 kontur
BINARY_INV -> 45 kontur

BINARY bilan bitta kontur — u butun FON. Harflar emas. BINARY_INV bilan 45 ta — mana harflar.

Bu xatoni deyarli hamma qiladi. Kontur topishdan oldin doim so'rang: obyektim oqmi?

RETR_EXTERNAL — faqat tashqi konturlar. Harf ichidagi teshiklar ("o" harfining ichi) alohida kontur sifatida qaytarilmaydi. Hujjat uchun odatda shu kerak.

CHAIN_APPROX_SIMPLE — to'g'ri chiziqni faqat ikki nuqta bilan saqlaydi. Xotira tejaydi.

Kontur atrofiga to'rtburchak

Bu — hujjatdan matn bloklarini ajratishning asosi:

python5 qator
for k in konturlar:
    x, y, w, h = cv2.boundingRect(k)
    if w * h < 100:            # juda kichik — shovqin
        continue
    print(f"blok: x={x} y={y} kenglik={w} balandlik={h}")

w * h < 100 filtri muhim. Busiz yuzlab mayda shovqin konturlari chiqadi.

Amaliy: matn bloklarini topish

Alohida harflar emas, butun qatorlarni topish uchun hiyla bor: avval harflarni gorizontal yo'nalishda "yopishtiramiz".

python14 qator
# Matn OQ bo'lishi kerak — shuning uchun INV
_, teskari = cv2.threshold(kulrang, 128, 255, cv2.THRESH_BINARY_INV)

# Keng va past yadro — harflarni yonma-yon ulaydi
qator_yadro = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 3))
qatorlar_rasm = cv2.dilate(teskari, qator_yadro, iterations=1)

qatorlar, _ = cv2.findContours(
    qatorlar_rasm, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for k in sorted(qatorlar, key=lambda c: cv2.boundingRect(c)[1]):
    x, y, w, h = cv2.boundingRect(k)
    if w * h >= 100:
        print(f"qator: x={x} y={y} w={w} h={h}")

Natija:

yaml3 qator
qator: x=32  y=33   w=324 h=30
qator: x=29  y=111  w=311 h=22
qator: x=29  y=171  w=275 h=22

45 ta harf -> 3 ta qator. Aynan hujjatdagi uchta qator: sarlavha, summa, sana.

(25, 3) — keng va past. Gorizontal yo'nalishda kuchli yoyadi, vertikalda deyarli yo'q. Natijada bir qatordagi harflar birlashadi, lekin qatorlar bir-biriga yopishmaydi.

Bu — hujjat tahlilining asosiy usuli. Yadro o'lchamini o'zgartirib, so'z / qator / paragraf darajasini boshqarasiz.

Qiyshiq hujjatni to'g'rilash

Telefonda olingan surat deyarli doim qiyshiq bo'ladi.

python16 qator
def burchakni_top(oq_qora):
    """Matn bloklarining o'rtacha burilish burchagini topadi."""
    koords = np.column_stack(np.where(oq_qora > 0))
    burchak = cv2.minAreaRect(koords)[-1]
    if burchak < -45:
        burchak = 90 + burchak
    return -burchak


def togrila(rasm, burchak):
    b, k = rasm.shape[:2]
    markaz = (k // 2, b // 2)
    M = cv2.getRotationMatrix2D(markaz, burchak, 1.0)
    return cv2.warpAffine(rasm, M, (k, b),
                          flags=cv2.INTER_CUBIC,
                          borderMode=cv2.BORDER_REPLICATE)

minAreaRect barcha matn piksellarini o'rab turgan eng kichik burchakli to'rtburchakni topadi. Uning burchagi — hujjatning qiyshiqligi.

Nega bu OCR uchun muhim: ko'p OCR modellari matn gorizontal deb hisoblaydi. 5 daraja qiyshiqlik ham aniqlikni sezilarli tushiradi.

To'liq quvur

python20 qator
def hujjatni_tayyorla(yol):
    rasm = cv2.imread(yol)
    if rasm is None:
        raise FileNotFoundError(yol)

    kulrang = cv2.cvtColor(rasm, cv2.COLOR_BGR2GRAY)

    # 1. Shovqinni yumshatish (chetlarni saqlagan holda)
    yumshoq = cv2.bilateralFilter(kulrang, 9, 75, 75)

    # 2. Adaptive threshold — notekis yorug'likka chidamli
    oq_qora = cv2.adaptiveThreshold(
        yumshoq, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY, 31, 10)

    # 3. Mayda shovqinni olib tashlash
    yadro = np.ones((2, 2), np.uint8)
    tozalangan = cv2.morphologyEx(oq_qora, cv2.MORPH_OPEN, yadro)

    return tozalangan

bilateralFilter nega oddiy blur emas? Oddiy xiralashtirish chetlarni ham yuvadi — harflar noaniq bo'ladi. Bilateral filtr tekis hududlarni yumshatadi, lekin chetlarni saqlaydi.

Sekinroq, lekin matn uchun ancha yaxshi.


Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar