Loyiha — hujjatdan ma'lumot ajratish
Bu bob butun kitobni birlashtiradi. 0-bobdagi hisob-faktura, 3-bobdagi tozalash, 7-bobdagi ma'lumot tayyorlash — hammasi bitta ishlaydigan quvurga aylanadi.
Vazifa: hujjat rasmidan tuzilmali ma'lumot olish.
rasm -> {"summa": "250 000", "sana": "2026-01-15"}
Diqqat: maqsad matn emas, maydonlar. Bu farq muhim.
Arxitektura
1. Rasmni yuklash va tekshirish 2. Tozalash <- 3-bob (adaptive threshold, morfologiya) 3. Matn bloklarini topish <- 3-bob (dilate + kontur) 4. Har bir blokni o'qish <- OCR kitobi 5. Maydonlarga ajratish <- BU BOB 6. Tekshirish va qaytarish
5-qadam eng ko'p unutiladigan qism. OCR sizga matn beradi.
Lekin "Summa: 250 000 som" degan satrdan 250000 sonini
ajratish — bu alohida ish.
1-qism: hujjat yasash (sinov uchun)
import cv2 import numpy as np def hisob_faktura_yasa(summa="250 000", sana="2026-01-15", raqam="INV-0042"): rasm = np.ones((360, 640, 3), dtype=np.uint8) * 255 F = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(rasm, "HISOB-FAKTURA", (40, 60), F, 1.1, (0,0,0), 2, cv2.LINE_AA) cv2.line(rasm, (40, 80), (600, 80), (0,0,0), 2) cv2.putText(rasm, f"Raqam: {raqam}", (40, 130), F, 0.7, (0,0,0), 2, cv2.LINE_AA) cv2.putText(rasm, f"Sana: {sana}", (40, 180), F, 0.7, (0,0,0), 2, cv2.LINE_AA) cv2.putText(rasm, f"Summa: {summa} som", (40, 230), F, 0.7, (0,0,0), 2, cv2.LINE_AA) cv2.putText(rasm, "Toshkent shahri", (40, 280), F, 0.7, (0,0,0), 2, cv2.LINE_AA) return rasm hujjat = hisob_faktura_yasa() cv2.imwrite("faktura.png", hujjat) print("yaratildi:", hujjat.shape)
2-qism: tozalash quvuri
def tayyorla(rasm): """3-bobdagi usullar — bitta funksiyada.""" kulrang = cv2.cvtColor(rasm, cv2.COLOR_BGR2GRAY) if rasm.ndim == 3 else rasm # Chetlarni saqlab shovqinni yumshatish yumshoq = cv2.bilateralFilter(kulrang, 9, 75, 75) # Matn OQ bo'lishi kerak — INV (3-bobdagi tuzoq) oq_matn = cv2.adaptiveThreshold( yumshoq, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 15) # Mayda shovqinni olib tashlash yadro = np.ones((2, 2), np.uint8) return cv2.morphologyEx(oq_matn, cv2.MORPH_OPEN, yadro) tayyor = tayyorla(hujjat) print("oq piksel foizi:", round(100 * (tayyor > 0).mean(), 2))
Natija: oq piksel foizi: 4.72
4.72% — matn hujjatning 5% ini egallaydi. Bu son foydali tekshiruv: 0 ga yaqin bo'lsa threshold juda qattiq (matn yo'qolgan), 50% ga yaqin bo'lsa juda yumshoq (shovqin qolgan).
THRESH_BINARY_INV — 3-bobdagi eng muhim saboq.
findContours oq hududlarni obyekt deb biladi, shuning uchun
matn oq bo'lishi shart.
3-qism: matn qatorlarini topish
def qatorlarni_top(tayyor, min_kenglik=40, min_balandlik=10): """Harflarni gorizontal ulab, qatorlarga ajratadi.""" yadro = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 3)) ulangan = cv2.dilate(tayyor, yadro, iterations=1) konturlar, _ = cv2.findContours( ulangan, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) qutilar = [] for k in konturlar: x, y, w, h = cv2.boundingRect(k) if w >= min_kenglik and h >= min_balandlik: qutilar.append((x, y, w, h)) # Yuqoridan pastga tartiblash — o'qish tartibi return sorted(qutilar, key=lambda q: q[1]) qatorlar = qatorlarni_top(tayyor) print(f"topilgan qator: {len(qatorlar)}") for x, y, w, h in qatorlar: print(f" x={x:>3} y={y:>3} w={w:>3} h={h:>2}")
Natija:
topilgan qator: 5 x= 32 y= 36 w=300 h=28 <- HISOB-FAKTURA (sarlavha, balandroq) x= 30 y=114 w=224 h=25 <- Raqam x= 30 y=164 w=244 h=20 <- Sana x= 30 y=214 w=276 h=20 <- Summa x= 29 y=263 w=198 h=21 <- Toshkent shahri
Aynan 5 ta qator — hujjatdagidek. Sarlavha h=28, qolganlari
h=20-25: shrift kattaligi farqi ko'rinib turibdi. Bu ma'lumotdan
sarlavhani ajratish uchun foydalanish mumkin.
sorted(..., key=lambda q: q[1]) — y bo'yicha tartiblash.
Kontur topish tartibi tasodifiy, o'qish esa yuqoridan pastga
bo'lishi kerak.
4-qism: OCR o'rniga — vaqtinchalik "soxta" o'qish
Haqiqiy OCR uchun OCR kitobiga qarang. Bu yerda quvurni sinash uchun oddiy funksiya ishlatamiz:
def matn_oqi(rasm_boragi): """HAQIQIY loyihada bu yerda Tesseract yoki CRNN turadi. Hozircha quvurni sinash uchun bo'sh joy qoldiramiz — OCR ni ulaganingizda faqat SHU funksiyani almashtirasiz. """ try: import pytesseract return pytesseract.image_to_string(rasm_boragi, lang="uzb").strip() except ImportError: return None
Bu tuzilish muhim: OCR — bitta almashtiriladigan funksiya. Tesseract, EasyOCR, o'z CRNN modelingiz — quvurning qolgani o'zgarmaydi.
5-qism: maydonlarni ajratish
Bu — bobning asosiy qismi. Matndan tuzilmali ma'lumot.
import re NAQSHLAR = { "raqam": r"raqam\s*[:\-]?\s*([A-Z]{2,4}[-\s]?\d+)", "sana": r"sana\s*[:\-]?\s*(\d{4}[-./]\d{2}[-./]\d{2})", "summa": r"summa\s*[:\-]?\s*([\d\s]+?)\s*(?:som|so'm|сум)", } def maydonlarni_ajrat(matn): """Matndan maydonlarni topadi. Topilmasa — None.""" natija = {} past = matn.lower() for nom, naqsh in NAQSHLAR.items(): m = re.search(naqsh, past, re.IGNORECASE) natija[nom] = m.group(1).strip() if m else None return natija sinov = """HISOB-FAKTURA Raqam: INV-0042 Sana: 2026-01-15 Summa: 250 000 som Toshkent shahri""" print(maydonlarni_ajrat(sinov))
Natija:
{'raqam': 'inv-0042', 'sana': '2026-01-15', 'summa': '250 000'}
Uchala maydon ham topildi. raqam kichik harfda — chunki
matn.lower() qilganmiz. Kerak bo'lsa .upper() qiling.
Nega regex, LLM emas? Hujjat formati barqaror bo'lsa, regex: tez, bepul, aniq va tushuntirib beriladi. LLM faqat format xilma-xil bo'lganda kerak.
6-qism: tozalash va tekshirish
OCR xato qiladi. Ajratilgan qiymatni tekshirish kerak.
from datetime import datetime def summani_tozala(xom): """'250 000' -> 250000. Xato bo'lsa None.""" if not xom: return None faqat_raqam = re.sub(r"[^\d]", "", xom) if not faqat_raqam: return None son = int(faqat_raqam) # Aql bovar qiladigan oraliq — OCR "0" ni "8" deb o'qishi mumkin return son if 100 <= son <= 10_000_000_000 else None def sanani_tekshir(xom): """Sana haqiqiymi? '2026-13-45' -> None.""" if not xom: return None for fmt in ("%Y-%m-%d", "%Y.%m.%d", "%Y/%m/%d"): try: d = datetime.strptime(xom, fmt) if 2000 <= d.year <= 2100: return d.strftime("%Y-%m-%d") except ValueError: continue return None print("summa :", summani_tozala("250 000")) print("buzuq :", summani_tozala("2")) print("sana :", sanani_tekshir("2026-01-15")) print("buzuq :", sanani_tekshir("2026-13-45"))
Natija:
summa : 250000 buzuq : None sana : 2026-01-15 buzuq : None
Ikkala buzuq qiymat ham None — bazaga kirmaydi. Mana shu
tekshiruv tizimni ishonchli qiladi.
2026-13-45 — 13-oy, 45-kun. OCR bunday narsa qaytarishi
mumkin. Tekshirmasangiz, baza ichiga axlat kiradi.
Oraliq tekshiruvi ham muhim: OCR "5" ni "6" deb o'qishi
mumkin, lekin 250000 o'rniga 2 qaytarsa — bu aniq xato.
7-qism: to'liq quvur
def hujjatni_qayta_ishla(yol_yoki_rasm): """Rasmdan tuzilmali ma'lumotgacha — to'liq quvur.""" if isinstance(yol_yoki_rasm, str): rasm = cv2.imread(yol_yoki_rasm) if rasm is None: return {"xato": f"fayl o'qilmadi: {yol_yoki_rasm}"} else: rasm = yol_yoki_rasm tayyor = tayyorla(rasm) qatorlar = qatorlarni_top(tayyor) if not qatorlar: return {"xato": "matn topilmadi — rasm juda xira bo'lishi mumkin"} # Har bir qatorni o'qish matnlar = [] for x, y, w, h in qatorlar: boragi = rasm[max(0, y-4):y+h+4, max(0, x-4):x+w+4] oqilgan = matn_oqi(boragi) if oqilgan: matnlar.append(oqilgan) if not matnlar: return {"xato": "OCR ishlamadi — pytesseract o'rnatilganmi?", "qatorlar_topildi": len(qatorlar)} xom_matn = "\n".join(matnlar) maydonlar = maydonlarni_ajrat(xom_matn) return { "matn": xom_matn, "raqam": maydonlar["raqam"], "sana": sanani_tekshir(maydonlar["sana"]), "summa": summani_tozala(maydonlar["summa"]), "qatorlar": len(qatorlar), }
Har bir qadamda xato holati qaytariladi — traceback emas. Foydalanuvchi nima bo'lganini bilishi kerak.
max(0, y-4) — bo'rtma (padding). Kontur harfning aynan
chegarasini beradi, OCR esa biroz bo'sh joy bilan yaxshiroq
ishlaydi. max(0, ...) esa manfiy indeksdan saqlaydi.
Sinash
natija = hujjatni_qayta_ishla("faktura.png") for kalit, qiymat in natija.items(): if kalit != "matn": print(f" {kalit}: {qiymat}")
pytesseract o'rnatilmagan kompyuterda natija:
xato: OCR ishlamadi — pytesseract o'rnatilganmi? qatorlar_topildi: 5
pytesseract o'rnatilmagan bo'lsa, xato maydoni chiqadi —
lekin qatorlar_topildi soni ko'rinadi. Ya'ni quvurning CV
qismi ishlayapti, faqat OCR yo'q.
Bu — yaxshi xato xabari namunasi: nima ishladi va nima ishlamadi — ikkalasi ham aytiladi.
Nimani yaxshilash mumkin
1. Qiyshiq hujjat. 3-bobdagi togrila() funksiyasini
tayyorla() boshiga qo'shing.
2. Jadval. Hozirgi kod qatorlarni topadi. Jadval uchun vertikal va gorizontal chiziqlarni alohida topish kerak:
def chiziqlarni_top(tayyor): """Jadval chiziqlarini ajratadi.""" gor_yadro = cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1)) ver_yadro = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40)) gorizontal = cv2.morphologyEx(tayyor, cv2.MORPH_OPEN, gor_yadro) vertikal = cv2.morphologyEx(tayyor, cv2.MORPH_OPEN, ver_yadro) return gorizontal, vertikal
Uzun va yupqa yadro faqat uzun chiziqlarni qoldiradi — matn yo'qoladi. Bu jadval katakchalarini topishning asosi.
3. Format o'zgarsa. Har bir yetkazib beruvchining fakturasi boshqacha. Ikki yo'l:
- har biri uchun alohida
NAQSHLARto'plami (aniq, lekin qo'lda) - VLM ga rasmni berib, JSON so'rash (moslashuvchan, lekin sekin va pullik)
Amalda: 80% hujjat uchun regex, qolgani uchun VLM.
Yakun
Siz qurgan narsa:
rasm -> tozalash -> qatorlar -> OCR -> maydonlar -> tekshirish -> JSON
Har bir qadam alohida funksiya va alohida sinaladi. OCR ni almashtirsangiz, qolgani o'zgarmaydi.
Bu — haqiqiy hujjat AI tizimining skeleti. Qolgani — sifatni oshirish va maydonlarni ko'paytirish.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.