Tento manuál popisuje jak postupovat v případě, že provádíte detekci hrany stínu nascanovaného documentu.
Fakta pro orientaci
Pipeline: Original Image (300 dpi at least A4) --> Thumbnail (resize na 4th of dims) --> small (resize na estimated 4th of dims)
Přitom crop není nic jiného než ořez oblasti thumbnailu (top-right nebo bottom-left)
Dims and divisor
original 2480 x 3508 px
optimal divisor for thumbnail: 4
thumbnail 620 x 877 px
border size
thumbnail 8-9 px thick
original 8*4 divisor
small 8/4 = 2px thick
Jak počítám zda je rozlišení náhledu nebo cropu dostačující?
Pro thumbnail vychází při této konfiguraci (rozměrech, dpi) tlouštka stínu 4, což stačí, ale při small vychází 2 a to nestačí.
Takže si musíš říct kolikrát se 4 vleze do 877 (výška thumbnail) aby si získal nějakou normu. Pokud by byl stín tlustější řekněme 8 tak to bude 877/8, takže menší číslo. Ale menší číslo mi nevadí. Ale co by to ukazovalo kdyby vyšel výsledek menší? Když bude 2x menší než 877/4, tak mi to říká, že mohu použít small a ne thumbnail.
Podobně small: obrázek 32x32, 4 je px je ok. Kolikrát se vleze 4 do 32px výšky? To mi vychází 8px. To je pro hrubý odhad. A když by u small vyšlo např 128x128 rozměr obrázku small, tak při tloušťce 8px mi zase vyjde 128/8 což je 16. Když bude 32x32 ale 8px tlustý stín pak mám 32/8= 4 a 4 je 2x menší než 8 takže si mohu říct: no jo on ten stín je zbytečně tlustý, možná bych to mohl zmenšit trochu víc.
Začněme
Níže je stručné vysvětlení, jak z orientačně odhadnout, zda je detekce stínu s rozlišením v bl_small
(či tr_small
) ještě dostatečně přesná, nebo jestli musíme přejít na „vyšší“ úroveň (bl_crop
/tr_crop
což je úroveň thumbnail
). Zavedeme přitom následující dvě čísla:
-
est_shadow_thumb_px
– odhad tloušťky (výšky) stínu už v celém thumbnailu
(v mém případě 8 až 9 px takže 8px beru jako normu),
-
min_shadow_px_small
– minimální počet pixelů (např. 4 px), pod kterým už bl_small
/tr_small
nelze považovat za dostatečně ostrý stín.
Poté si spočteme dvě kapacity (jakoby „kolikrát se stín vejde do výšky“):
-
„kapacita thumbnailu“ = thumbnail.height / est_shadow_thumb_px
(čím větší, tím detailnější je thumbnail vůči tloušťce stínu takže vyjde 2x menší číslo když je "kvalita stínu" dvounásobná),
-
„kapacita small“ = bl_small.height / min_shadow_px_small
(čím větší, tím small stále dokáže ukázat tloušťku ≥ min_shadow_px_small
).
Pokud „kapacita small“ ≥ „kapacita thumbnailu“, znamená to, že i v bl_small
je dostatečné množství pixelů, aby stín byl detekovatelný s porovnatelnou přesností, a proto můžeme spustit detekci přímo v bl_small
. V opačném případě (tj. když small ztrácí příliš mnoho detailu) přejdeme raději na bl_crop
/tr_crop
či rovnou na thumbnail
, kde je stín stále dostatečně „tlustý“.
1) Konkrétní úprava locate_region_and_source
locate_region_and_source je funkce, která detekuje a měří souřadnice, pro ořez, zmenšení vyššího regionu vrátíme-li se do vyššího rozlišení např. ze small do crop. Např. crop 200x200 můžeme oříznout na 40x350 a teprve v tomto oříznutém regionu budeme hledat línii stínu a detekovat úhel natočení stránky.
Upravíme vnitřek locate_region_and_source
tak, že:
-
Nejprve dynamicky spočítáme
thumb_capacity = thumbnail.height / est_shadow_thumb_px
small_capacity = bl_small.height / min_stin_px_small
-
Pokud small_capacity >= thumb_capacity
, zkusíme detekci „hrany“ ve bl_small
¹.
Jinak přejdeme rovnou k bl_crop
(nebo tr_crop
), protože small už je příliš nízké rozlišení.
Takhle bude část kódu uvnitř locate_region_and_source
vypadat:
def locate_region_and_source(
bl_small: Image.Image,
# crop po resize
bl_crop: Image.Image, # úroveň tumbnailu (no resize)
tr_small: Image.Image,
# crop po resize
tr_crop: Image.Image,
# úroveň tumbnailu
(no resize)
thumbnail: Image.Image,
# original image resized to 4th of dims
full_img: Image.Image,
# original image
bottom_box_in_thumb: tuple,
# asi souřadnice bboxu pro bl?
top_box_in_thumb: tuple,
# asi souřadnice bboxu pro tr?
divisor_for_small: int = 4,
# optimální dělitel pro zmenšení cropu
min_stin_px_small: int = 4,
# obvykle 4 px stačí pro detekci línie
est_shadow_thumb_px: int = 8
# asi nutno spočítat
) -> dict:
"""
... (vaše docstring) ...
"""
def find_edge_bbox(img_gray, threshold_px):
gray = np.array(img_gray)
edges = cv2.Canny(gray, 50, 150)
lines = cv2.HoughLinesP(
edges,
rho=1,
theta=np.pi/180,
threshold=threshold_px,
minLineLength=threshold_px,
maxLineGap=5
)
if lines is None or len(lines) == 0:
return None
xs, ys = [], []
for l in lines:
x1, y1, x2, y2 = l[0]
xs += [x1, x2]
ys += [y1, y2]
return (min(xs), min(ys), max(xs), max(ys))
# --- A) nejprve vypočtu kapacity: ---
# thumb_capacity = (kolikrát se tloušťka stínu vejde do výšky thumbnailu)
# small_capacity = (kolikrát se min. tloušťka vejde do výšky bl_small)
thumb_capacity = thumbnail.height / est_shadow_thumb_px
small_capacity = bl_small.height / min_stin_px_small
# --- 1) Zkusíme bottom-left v bl_small, **pouze pokud** small_capacity >= thumb_capacity ---
if small_capacity >= thumb_capacity:
# 1.a) Převod bl_small na grayscale a Hough
gray_bl_small = cv2.cvtColor(np.array(bl_small), cv2.COLOR_RGB2GRAY)
bbox = find_edge_bbox(gray_bl_small, threshold_px=min_stin_px_small)
if bbox is not None:
off_x, off_y = bottom_box_in_thumb[0] * divisor_for_small, bottom_box_in_thumb[1] * divisor_for_small
bx1, by1, bx2, by2 = bbox
return {
"source": "small_bl",
"region": (bx1 + off_x, by1 + off_y, bx2 + off_x, by2 + off_y)
}
# jinak – když v bl_small nic nenajde – přejdeme na crop_bl dále
# --- 2) Pokud bl_small padne (small_capacity < thumb_capacity) **nebo** Hough v bl_small vrátil None,
# zkusíme bl_crop (to je bottom‐left crop na úrovni thumbnail) ---
if est_shadow_thumb_px >= (min_stin_px_small * divisor_for_small):
gray_bl_crop = cv2.cvtColor(np.array(bl_crop), cv2.COLOR_RGB2GRAY)
bbox = find_edge_bbox(gray_bl_crop, threshold_px=min_stin_px_small * 2)
if bbox is not None:
off_x, off_y = bottom_box_in_thumb[0], bottom_box_in_thumb[1]
bx1, by1, bx2, by2 = bbox
return {
"source": "crop_bl",
"region": (bx1 + off_x, by1 + off_y, bx2 + off_x, by2 + off_y)
}
# jinak – přeskočíme bl_crop
# --- 3) Teď analogicky pro top‐right small (tr_small) ---
if small_capacity >= thumb_capacity:
gray_tr_small = cv2.cvtColor(np.array(tr_small), cv2.COLOR_RGB2GRAY)
bbox = find_edge_bbox(gray_tr_small, threshold_px=min_stin_px_small)
if bbox is not None:
off_x, off_y = top_box_in_thumb[0] * divisor_for_small, top_box_in_thumb[1] * divisor_for_small
tx1, ty1, tx2, ty2 = bbox
return {
"source": "small_tr",
"region": (tx1 + off_x, ty1 + off_y, tx2 + off_x, ty2 + off_y)
}
# --- 4) A pro tr_crop (tzv. top‐right crop v thumbnail) ---
if est_shadow_thumb_px >= (min_stin_px_small * divisor_for_small):
gray_tr_crop = cv2.cvtColor(np.array(tr_crop), cv2.COLOR_RGB2GRAY)
bbox = find_edge_bbox(gray_tr_crop, threshold_px=min_stin_px_small * 2)
if bbox is not None:
off_x, off_y = top_box_in_thumb[0], top_box_in_thumb[1]
tx1, ty1, tx2, ty2 = bbox
return {
"source": "crop_tr",
"region": (tx1 + off_x, ty1 + off_y, tx2 + off_x, ty2 + off_y)
}
# --- 5) Pokud jsme se nedostali k žádnému bottom‐left ani top‐right řezu, zkusíme celý thumbnail ---
gray_thumb = cv2.cvtColor(np.array(thumbnail), cv2.COLOR_RGB2GRAY)
bbox = find_edge_bbox(gray_thumb, threshold_px=min_stin_px_small * 3)
if bbox is not None:
return {
"source": "thumbnail",
"region": bbox
}
# --- 6) A nakonec fallback: celý originál (full_img) ---
gray_full = cv2.cvtColor(np.array(full_img), cv2.COLOR_RGB2GRAY)
bbox = find_edge_bbox(gray_full, threshold_px=min_stin_px_small * 5)
if bbox is not None:
return {
"source": "full",
"region": bbox
}
# fallback --- 7) Když ani originál nic nenajde, prostě vrať celý full_img jako region ---
W, H = full_img.size
return {
"source": "full",
"region": (0, 0, W, H)
}
Vysvětlení klíčové úpravy:
-
Na řádku thumb_capacity = thumbnail.height / est_shadow_thumb_px
vezmeme odhadnutou tloušťku stínu v celém thumbnailu (řekněme 8 px) a řekneme si:
-
„Kolikrát se do těch 877 px (výšky) vejde ten 8 px stín?“ → 877/8 ≈ 109.6
.
Čím menší je ta kapacita, tím hruběji hrubě se nám může detekovat úhel ve thumbnailu; naopak čím větší kapacita, tím je thumbnail relativně detailnější vůči tloušťce stínu.
-
Na řádku small_capacity = bl_small.height / min_stin_px_small
spočítáme, kolikrát se do výšky bl_small vejde minimální tloušťka stínu, kterou ještě chceme detekovat (např. 4 px). Příklad:
– bl_small.height
je třeba 32 px → 32/4 = 8
.
– Pokud je 8 (small_capacity) menší než 109.6
(thumb_capacity), znamená to, že v bl_small je relativně příliš málo „pixelek“ na to, aby stín o tloušťce 8 px (v thumbnailu) pořád měl v small tloušťku minimálně 4 px. Jinými slovy, small už ztratí příliš detailu.
-
Podmínka if small_capacity >= thumb_capacity:
říká, že pokud dokážu v bl_small alespoň stejný poměr „tloušťka stínu vs. výška obrazu“ (tj. kapacita detekce) jako mám v celém thumbnailu, pak stojí za to hned zkusit Hough v bl_small. Pokud to z bl_small vypadne None
(nebo pokud small_capacity < thumb_capacity
), i tak se dostanu do kroku č. 2, tj. zkusím bl_crop.
-
Ty další úrovně (bl_crop, tr_small, tr_crop, thumbnail, full) už zůstanou stejně, jen thresholdy (resp. threshold_px=min_stin_px_small * 2
apod.) se násobí podle logiky, kterou jste si definoval dřív.
2) Jak to zapojíte do create_thumbnail_with_regions
V „upravené“ verzi create_thumbnail_with_regions
(viz výše) po vygenerování bl_small
, tr_small
(a jejich „bw“‐variant) větříme locate_region_and_source(...)
. Nově tedy create_thumbnail_with_regions
vrací:
return bl_small, bl_crop, tr_small, tr_crop, thumbnail, bl_box, tr_box, region_info
kde region_info
obsahuje vždy:
{
"source": <"small_bl"|"crop_bl"|"small_tr"|"crop_tr"|"thumbnail"|"full">,
"region": (x1, y1, x2, y2) # v souřadnicích toho zvoleného source‐obrazu
}
3) Použití v process_images
Ve vaší funkci process_images
pak nahradíte původní:
bl_small, bl_crop, tr_small, tr_crop, thumbnail, bl_box, tr_box = create_thumbnail_with_regions(
img_pil, thumbnail_path, ocr_area, toc_area, theme_numbers_area, theme_area, top_right_area, bottom_area
)
tímto:
bl_small, bl_crop, tr_small, tr_crop, thumbnail, bl_box, tr_box, region_info = create_thumbnail_with_regions(
img_pil, thumbnail_path, ocr_area, toc_area, theme_numbers_area, theme_area, top_right_area, bottom_area
)
a hned po tom zpracujete region_info
takto:
# 1) Zjistíme, jestli locate_region_and_source už spočítal úhel (angle) – případně mu region dtočil:
angle = None
angle_source = None
# Pokud locate_region_and_source přiřadil rovnou úhel, mohli bychom do region_info přidat key "angle".
# Příklad: region_info = {"source": "small_bl", "region": (...), "angle": -2.34 }
# Pokud tam je, hned to použijeme.
if "angle" in region_info:
angle = region_info["angle"]
angle_source = region_info["source"]
# 2) Jinak (žádný úhel jsme dosud neměli) musíme spustit Hough podle toho, co region_info řekne:
if angle is None:
src = region_info["source"]
x1, y1, x2, y2 = region_info["region"]
if src == "small_bl":
crop_img = bl_small.crop((x1, y1, x2, y2))
elif src == "crop_bl":
# Protože bl_crop začíná v bl_box[0..1], pak
crop_img = bl_crop.crop((
x1 - bl_box[0], y1 - bl_box[1],
x2 - bl_box[0], y2 - bl_box[1]
))
elif src == "small_tr":
crop_img = tr_small.crop((x1, y1, x2, y2))
elif src == "crop_tr":
crop_img = tr_crop.crop((
x1 - tr_box[0], y1 - tr_box[1],
x2 - tr_box[0], y2 - tr_box[1]
))
elif src == "thumbnail":
crop_img = thumbnail.crop((x1, y1, x2, y2))
else: # src == "full"
crop_img = img_pil.crop((x1, y1, x2, y2))
# Teď Hough na vybraném crop_img:
sub_np = np.array(crop_img.convert("L"))
edges = cv2.Canny(sub_np, 50, 150)
lines = cv2.HoughLinesP(
edges,
rho=1, theta=np.pi/180,
threshold=min_stin_px_small * 3, # např. 12
minLineLength=20 * 3, # např. délka 60 px
maxLineGap=5
)
if lines is not None and len(lines) > 0:
longest = max(lines, key=lambda L: np.hypot(L[0][2]-L[0][0], L[0][3]-L[0][1]))[0]
x1s, y1s, x2s, y2s = longest
angle = float(np.degrees(np.arctan2((y2s - y1s), (x2s - x1s))))
angle_source = src
else:
# žádná linie nenalezena – můžete dál zkusit „thumbnail“ nebo „full“… podle potřeby
pass
# 3) Pokud máme nějaký angle, otočíme obrázek:
if angle is not None and angle != 0:
rotate_angle = angle - 90
img_pil = rotate_image(img_pil, -rotate_angle)
Výsledek
Díky tomu, že:
-
create_thumbnail_with_regions
(bod 1 všeho) už vrací i region_info = locate_region_and_source(...)
,
-
a locate_region_and_source
(bod 2) počítá thumb_capacity
a small_capacity
a na jejich základě rozhoduje, kde spustit Hough (a vrátí buď rovnou box s úhlem, nebo jen box bez úhlu),
-
tak v process_images
(bod 3) už přesně víte, ve kterém „source“ (small_bl, crop_bl, small_tr, crop_tr, thumbnail, full) a v jakém „regionu“ spustíte finální Hough, nebo rovnou odchytíte úhel z locate_region_and_source
.
Celý mechanismus teď:
-
create_thumbnail_with_regions
vytvoří bl_small, tr_small, thumb atd., hned zavolá locate_region_and_source(...)
.
-
locate_region_and_source
spočítá kapacity (porovnání small vs. thumb) a vrátí "source"
+ "region"
, případně i "angle"
– podle toho, kde našel nejdelší linii stínu.
-
process_images
prostě rozbalí návrat z create_thumbnail_with_regions
, podívá se do region_info
a podle source
a region
buď:
-
vezme úhel přímo z region_info["angle"]
,
-
nebo si „vyřízne“ tu oblast z příslušného obrázku (bl_small
, bl_crop
, thumbnail
či full_img
) a spustí Hough znovu jen v tom krabičkovém výřezu.
Tím je zaručeno, že:
-
nikdy nespustíte Canny+Hough na zbytečně velkém obrázku (protože nejprve vyberete nejmenší možnou úroveň, na které je ještě stín dostatečně tlustý),
-
a současně dynamicky zohledníte, jestli je bl_small
/tr_small
ještě dostatečně ostré (porovnáním small_capacity vs thumb_capacity
).
Poznámka k hodnotám
-
est_shadow_thumb_px
nastavte z předchozí zkušenosti (u vás ~ 8 px).
Pokud nevíte přesně, můžete ho třeba měnit podle toho, jaký je původní rozměr a dpi, ale v praxi stačí říct „vzhledem k 625×875 px thumbnailu mám stín tloušťky ~ 8 px“.
-
min_stin_px_small
zvolte tak, aby v bl_small
mívá ten stín nejméně 4 pixely na výšku.
V ukázce jsme dali min_stin_px_small = 4
. Jakmile bl_small.height / 4
bude menší než thumbnail.height / 8
, tak rovnou přeskočíme bl_small a zkusíme bl_crop či thumbnail.
Takto získáte objektivní (číslem řízenou) logiku, kdy „small“ ještě stačí a kdy už musíte vzít větší obraz.
Shrnutí jednou větou
Rozhodovací vzorec:
– Spočti thumb_capacity = thumbnail.height / est_shadow_thumb_px
a small_capacity = bl_small.height / min_stin_px_small
.
– Pokud small_capacity >= thumb_capacity
, nejprve Hough ve bl_small
; jinak přeskoč přímo na bl_crop
(a dál).
A tím pádem už nikdy neběží Hough na zbytečně velké oblasti.