|
|
@@ -65,15 +65,13 @@ def slice_grid(image: np.ndarray, rows: int, cols: int) -> list[np.ndarray]:
|
|
65
|
65
|
|
|
66
|
66
|
|
|
67
|
67
|
def add_border(image: np.ndarray, border_px: int) -> np.ndarray:
|
|
68
|
|
- return cv2.copyMakeBorder(
|
|
69
|
|
- image,
|
|
70
|
|
- top=border_px,
|
|
71
|
|
- bottom=border_px,
|
|
72
|
|
- left=border_px,
|
|
73
|
|
- right=border_px,
|
|
74
|
|
- borderType=cv2.BORDER_CONSTANT,
|
|
75
|
|
- value=BORDER_COLOR,
|
|
76
|
|
- )
|
|
|
68
|
+ if len(image.shape) == 2:
|
|
|
69
|
+ return cv2.copyMakeBorder(image, top=border_px, bottom=border_px,
|
|
|
70
|
+ left=border_px, right=border_px,
|
|
|
71
|
+ borderType=cv2.BORDER_CONSTANT, value=255)
|
|
|
72
|
+ return cv2.copyMakeBorder(image, top=border_px, bottom=border_px,
|
|
|
73
|
+ left=border_px, right=border_px,
|
|
|
74
|
+ borderType=cv2.BORDER_CONSTANT, value=BORDER_COLOR)
|
|
77
|
75
|
|
|
78
|
76
|
|
|
79
|
77
|
def save_page(image: np.ndarray, path: Path) -> None:
|
|
|
@@ -115,8 +113,14 @@ def parse_slice(value: str) -> tuple[int, int]:
|
|
115
|
113
|
return cols, rows
|
|
116
|
114
|
|
|
117
|
115
|
|
|
|
116
|
+def _to_gray(image: np.ndarray) -> np.ndarray:
|
|
|
117
|
+ if len(image.shape) == 2:
|
|
|
118
|
+ return image
|
|
|
119
|
+ return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
|
120
|
+
|
|
|
121
|
+
|
|
118
|
122
|
def is_empty(image: np.ndarray) -> bool:
|
|
119
|
|
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
|
123
|
+ gray = _to_gray(image)
|
|
120
|
124
|
dark_pixels = (gray < EMPTY_DARK_THRESHOLD).sum()
|
|
121
|
125
|
threshold = int(image.shape[0] * image.shape[1] * EMPTY_DARK_RATIO)
|
|
122
|
126
|
return dark_pixels <= threshold
|
|
|
@@ -126,7 +130,7 @@ def _binarize(image: np.ndarray) -> np.ndarray:
|
|
126
|
130
|
"""Бинаризация: серый → Гаусс-блюр → Otsu-порог → морф.закрытие.
|
|
127
|
131
|
При слишком низком пороге Otsu (<50) — адаптивный порог (Gaussian, окно 31).
|
|
128
|
132
|
Морфологическое закрытие (3×3) склеивает фрагменты букв в непрерывные регионы."""
|
|
129
|
|
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
|
133
|
+ gray = _to_gray(image)
|
|
130
|
134
|
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
|
|
131
|
135
|
otsu_th, binary = cv2.threshold(
|
|
132
|
136
|
blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU,
|
|
|
@@ -241,7 +245,7 @@ def _apply_perspective(image: np.ndarray, rect: np.ndarray) -> np.ndarray:
|
|
241
|
245
|
|
|
242
|
246
|
|
|
243
|
247
|
def unwarp_image(image: np.ndarray) -> np.ndarray:
|
|
244
|
|
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
|
248
|
+ gray = _to_gray(image)
|
|
245
|
249
|
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
|
246
|
250
|
edges = cv2.Canny(blurred, 50, 150)
|
|
247
|
251
|
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
@@ -266,6 +270,8 @@ def unwarp_image(image: np.ndarray) -> np.ndarray:
|
|
266
|
270
|
|
|
267
|
271
|
def denoise_image(image: np.ndarray) -> np.ndarray:
|
|
268
|
272
|
"""Non-Local Means denoising. Убирает шум, сохраняя границы символов."""
|
|
|
273
|
+ if len(image.shape) == 2:
|
|
|
274
|
+ return cv2.fastNlMeansDenoising(image, None, 10, 7, 21)
|
|
269
|
275
|
return cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21)
|
|
270
|
276
|
|
|
271
|
277
|
|