Przeglądaj źródła

added --denoise option; border is now 5 pix by default

master
Evgeniy Ierusalimov 3 dni temu
rodzic
commit
b6c41b31c6
5 zmienionych plików z 50 dodań i 8 usunięć
  1. 9
    4
      README.md
  2. 14
    2
      src/image_prepare.py
  3. 2
    0
      src/split/__init__.py
  4. 24
    1
      src/split/slicer.py
  5. 1
    1
      tests/test_post_crop.py

+ 9
- 4
README.md Wyświetl plik

@@ -61,8 +61,9 @@ src/image_ocr.py           ← Stage 2: OCR + Markdown
61 61
 | `--slice-auto` / `-a` | Автоопределение сетки | — |
62 62
 | `--output-dir` / `-o` | Каталог для страниц | `.` |
63 63
 | `--pre-rotate` / `-r` | Поворот: 90, 180, 270 | — |
64
-| `--border` / `-b` | Белая рамка (px) | 50 |
64
+| `--border` / `-b` | Белая рамка (px) | 5 |
65 65
 | `--unwarp` / `-u` | Выпрямить перспективные искажения (Canny → 4-угольный контур → warp) | OFF |
66
+| `--denoise` | Убрать шум сканера (Non-Local Means) | OFF |
66 67
 | `--post-crop` / `--no-post-crop` | Обрезка по контенту | ON |
67 68
 
68 69
 Пример:
@@ -70,6 +71,7 @@ src/image_ocr.py           ← Stage 2: OCR + Markdown
70 71
 python -m src.image_prepare -i scan.jpg -s 3:3 -b 10 -o pages/
71 72
 python -m src.image_prepare -i scan.jpg --slice-auto
72 73
 python -m src.image_prepare -i photo.jpg --unwarp -s 2:2 -b 10
74
+python -m src.image_prepare -i old_scan.jpg --denoise -s 2:2 -b 10
73 75
 ```
74 76
 
75 77
 <details>
@@ -105,12 +107,15 @@ python -m src.image_prepare -i photo.jpg --unwarp -s 2:2 -b 10
105 107
 │                                                         (белая рамка /       │
106 108
 │                                                         отступ при обрезке   │
107 109
 │                                                         содержимого, по      │
108
-│                                                         умолчанию 50)        │
109
-│                                                         [default: 50]        │
110
+│                                                         умолчанию 5) 
111
+│                                                         [default: 5] 
110 112
 │ --unwarp      -u                                        Выпрямить            │
111 113
 │                                                         перспективные        │
112 114
 │                                                         искажения перед      │
113 115
 │                                                         разрезанием          │
116
+│ --denoise                                                Убрать шум сканера  │
117
+│                                                         (Non-Local Means)   │
118
+│                                                         перед разрезанием   │
114 119
 │ --post-crop   -c  --no-post-crop                        Обрезать каждую      │
115 120
 │                                                         страницу по границам │
116 121
 │                                                         полезного            │
@@ -387,7 +392,7 @@ src/
387 392
 - LLM/RAG
388 393
 - Поиск по документам
389 394
 - Многопоточность
390
-- Автоматический deskew / denoise
395
+- Автоматический deskew
391 396
 
392 397
 ---
393 398
 

+ 14
- 2
src/image_prepare.py Wyświetl plik

@@ -11,6 +11,7 @@ from src.cli_utils import run_main
11 11
 from src.split.slicer import (
12 12
     DEFAULT_BORDER_PX,
13 13
     VALID_ROTATIONS,
14
+    denoise_image,
14 15
     detect_grid,
15 16
     load_image,
16 17
     parse_slice,
@@ -60,6 +61,7 @@ def _resolve_grid(
60 61
     input_path: Path,
61 62
     pre_rotate: int | None,
62 63
     unwarp: bool,
64
+    denoise: bool,
63 65
 ) -> tuple[int, int]:
64 66
     if slice_str is not None and slice_auto:
65 67
         raise typer.BadParameter(
@@ -74,6 +76,8 @@ def _resolve_grid(
74 76
             image = rotate_image(image, pre_rotate)
75 77
         if unwarp:
76 78
             image = unwarp_image(image)
79
+        if denoise:
80
+            image = denoise_image(image)
77 81
         cols, rows = detect_grid(image)
78 82
         typer.echo(f"Определена сетка: {cols}×{rows}")
79 83
         return cols, rows
@@ -136,7 +140,7 @@ def slice_pages(
136 140
             "--border",
137 141
             "-b",
138 142
             min=0,
139
-            help="Отступ в пикселях (белая рамка / отступ при обрезке содержимого, по умолчанию 50)",
143
+            help="Отступ в пикселях (белая рамка / отступ при обрезке содержимого, по умолчанию 5)",
140 144
         ),
141 145
     ] = DEFAULT_BORDER_PX,
142 146
     unwarp: Annotated[
@@ -147,6 +151,13 @@ def slice_pages(
147 151
             help="Выпрямить перспективные искажения перед разрезанием",
148 152
         ),
149 153
     ] = False,
154
+    denoise: Annotated[
155
+        bool,
156
+        typer.Option(
157
+            "--denoise",
158
+            help="Убрать шум сканера (Non-Local Means) перед разрезанием",
159
+        ),
160
+    ] = False,
150 161
     post_crop: Annotated[
151 162
         bool,
152 163
         typer.Option(
@@ -159,7 +170,7 @@ def slice_pages(
159 170
     """Подготовить отсканированное изображение: разрезать по сетке, обрезать, добавить рамку."""
160 171
     input_path = _validate_input(input)
161 172
     _validate_rotation(pre_rotate)
162
-    cols, rows = _resolve_grid(slice, slice_auto, input_path, pre_rotate, unwarp)
173
+    cols, rows = _resolve_grid(slice, slice_auto, input_path, pre_rotate, unwarp, denoise)
163 174
 
164 175
     output_paths = process_image(
165 176
         input_path=input_path,
@@ -170,6 +181,7 @@ def slice_pages(
170 181
         border_px=border,
171 182
         post_crop=post_crop,
172 183
         unwarp=unwarp,
184
+        denoise=denoise,
173 185
     )
174 186
 
175 187
     typer.echo(f"Сохранено {len(output_paths)} страниц в {output_dir.resolve()}")

+ 2
- 0
src/split/__init__.py Wyświetl plik

@@ -1,6 +1,7 @@
1 1
 from src.split.slicer import (
2 2
     DEFAULT_BORDER_PX,
3 3
     VALID_ROTATIONS,
4
+    denoise_image,
4 5
     detect_grid,
5 6
     load_image,
6 7
     parse_slice,
@@ -12,6 +13,7 @@ from src.split.slicer import (
12 13
 __all__ = [
13 14
     "DEFAULT_BORDER_PX",
14 15
     "VALID_ROTATIONS",
16
+    "denoise_image",
15 17
     "detect_grid",
16 18
     "load_image",
17 19
     "parse_slice",

+ 24
- 1
src/split/slicer.py Wyświetl plik

@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
10 10
 
11 11
 SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({".jpg", ".jpeg", ".png", ".tiff", ".tif"})
12 12
 PAGE_NUMBER_WIDTH: int = 2
13
-DEFAULT_BORDER_PX: int = 50
13
+DEFAULT_BORDER_PX: int = 5
14 14
 BORDER_COLOR: tuple[int, int, int] = (255, 255, 255)
15 15
 VALID_ROTATIONS: frozenset[int] = frozenset({90, 180, 270})
16 16
 DENSITY_RATIO: float = 0.005
@@ -262,6 +262,25 @@ def unwarp_image(image: np.ndarray) -> np.ndarray:
262 262
     return image
263 263
 
264 264
 
265
+def denoise_image(image: np.ndarray) -> np.ndarray:
266
+    """Non-Local Means denoising. Убирает шум, сохраняя границы символов."""
267
+    return cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21)
268
+
269
+
270
+def binarize_image(image: np.ndarray) -> np.ndarray:
271
+    """Адаптивная бинаризация для неравномерной подсветки."""
272
+    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
273
+    binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
274
+                                    cv2.THRESH_BINARY, 31, 10)
275
+    return cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
276
+
277
+
278
+def upscale_image(image: np.ndarray, factor: int = 2) -> np.ndarray:
279
+    """Увеличение для мелкого текста (бикубическая интерполяция)."""
280
+    h, w = image.shape[:2]
281
+    return cv2.resize(image, (w * factor, h * factor), interpolation=cv2.INTER_CUBIC)
282
+
283
+
265 284
 def process_image(
266 285
     input_path: Path,
267 286
     rows: int | None = None,
@@ -271,6 +290,7 @@ def process_image(
271 290
     border_px: int = DEFAULT_BORDER_PX,
272 291
     post_crop: bool = False,
273 292
     unwarp: bool = False,
293
+    denoise: bool = False,
274 294
 ) -> list[Path]:
275 295
     image = load_image(input_path)
276 296
 
@@ -280,6 +300,9 @@ def process_image(
280 300
     if unwarp:
281 301
         image = unwarp_image(image)
282 302
 
303
+    if denoise:
304
+        image = denoise_image(image)
305
+
283 306
     if rows is None or cols is None:
284 307
         cols, rows = detect_grid(image)
285 308
 

+ 1
- 1
tests/test_post_crop.py Wyświetl plik

@@ -121,7 +121,7 @@ class TestProcessImagePostCrop:
121 121
             result = process_image(Path(path), 1, 1, Path(tmp), post_crop=False)
122 122
             assert len(result) == 1
123 123
             loaded = cv2.imread(str(result[0]))
124
-            assert loaded.shape == (400, 500, 3)
124
+            assert loaded.shape == (310, 410, 3)
125 125
 
126 126
     def test_post_crop_e2e_2x2(self) -> None:
127 127
         image = create_test_image(400, 400)

Ładowanie…
Anuluj
Zapisz