Loyiha: hujjat skaneri
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.
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))
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.
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)
20 ta tasodifiy tartibda ham bir xil natija: True
| burchak | topilgan | haqiqiy | xato, 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.
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}")
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:
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.")
o'lcham: (575, 772)

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.
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)))}, ]))
skaner natijasi — aniqlik 76.7%, to'liqlik 86.0%, F1 81.1%
| usul | aniqlik % | to'liqlik % | F1 % |
|---|---|---|---|
| global 140 | 81.9 | 64.2 | 72 |
| Otsu | 78.6 | 80.6 | 79.6 |
| adaptive (skaner) | 76.7 | 86 | 81.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:
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)))}, ]))
| burchaklar | aniqlik % | to'liqlik % | F1 % |
|---|---|---|---|
| qo'lda yozilgan (aniq) | 91.3 | 99.3 | 95.2 |
| algoritm topgan (~1.2 px xato) | 76.7 | 86 | 81.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:
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_CLOSE | aniqlik % | to'liqlik % | F1 % |
|---|---|---|---|
| yo'q | 76.7 | 86 | 81.1 |
| bor | 52.3 | 60.5 | 56.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:
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 %']}%")
eng yaxshi: blockSize=31, C=20, F1=81.4%
| blockSize | C | aniqlik % | to'liqlik % | F1 % |
|---|---|---|---|---|
| 31 | 20 | 78.2 | 84.9 | 81.4 |
| 15 | 10 | 79.1 | 83.4 | 81.2 |
| 31 | 15 | 76.7 | 86 | 81.1 |
| 15 | 15 | 80.3 | 81.9 | 81.1 |
| 51 | 20 | 75.7 | 87.2 | 81 |
| 15 | 5 | 77.4 | 84.6 | 80.9 |
| 51 | 15 | 74.2 | 88.6 | 80.8 |
| 31 | 10 | 75.2 | 87.2 | 80.8 |
7-qadam: mustahkamlik sinovi
Skaner faqat bitta rasmda ishlasa, u hali dastur emas. Uni buzilgan kirishlar bilan sinaymiz:
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))
| kirish | burchak topildimi | o'lcham | F1 % |
|---|---|---|---|
| asl | True | (640, 900) | 81.1 |
| kichraytirilgan (0.5x) | True | (640, 900) | 82.4 |
| burilgan 10° | True | (640, 900) | 81.9 |
| shovqinli | True | (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
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")
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.
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
burchaklarni_topga 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?- Skanerga avtomatik burish qo'shing: agar hujjat yotiq chiqsa (w > h), uni 90° buring.
hujjat.jpgni o'z telefoningizdagi hujjat surati bilan almashtiring. Qaysi qadam birinchi bo'lib buziladi?- Skanerni papkadagi hamma rasmga qo'llaydigan va natijani PDF ga yig'adigan skript yozing
(
img2pdfyokiPillowbilan).
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.