Procházet zdrojové kódy

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

master
Evgeniy Ierusalimov před 3 dny
rodič
revize
b6c41b31c6
5 změnil soubory, kde provedl 50 přidání a 8 odebrání
  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 Zobrazit soubor

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

+ 14
- 2
src/image_prepare.py Zobrazit soubor

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

+ 2
- 0
src/split/__init__.py Zobrazit soubor

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

+ 24
- 1
src/split/slicer.py Zobrazit soubor

10
 
10
 
11
 SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({".jpg", ".jpeg", ".png", ".tiff", ".tif"})
11
 SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({".jpg", ".jpeg", ".png", ".tiff", ".tif"})
12
 PAGE_NUMBER_WIDTH: int = 2
12
 PAGE_NUMBER_WIDTH: int = 2
13
-DEFAULT_BORDER_PX: int = 50
13
+DEFAULT_BORDER_PX: int = 5
14
 BORDER_COLOR: tuple[int, int, int] = (255, 255, 255)
14
 BORDER_COLOR: tuple[int, int, int] = (255, 255, 255)
15
 VALID_ROTATIONS: frozenset[int] = frozenset({90, 180, 270})
15
 VALID_ROTATIONS: frozenset[int] = frozenset({90, 180, 270})
16
 DENSITY_RATIO: float = 0.005
16
 DENSITY_RATIO: float = 0.005
262
     return image
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
 def process_image(
284
 def process_image(
266
     input_path: Path,
285
     input_path: Path,
267
     rows: int | None = None,
286
     rows: int | None = None,
271
     border_px: int = DEFAULT_BORDER_PX,
290
     border_px: int = DEFAULT_BORDER_PX,
272
     post_crop: bool = False,
291
     post_crop: bool = False,
273
     unwarp: bool = False,
292
     unwarp: bool = False,
293
+    denoise: bool = False,
274
 ) -> list[Path]:
294
 ) -> list[Path]:
275
     image = load_image(input_path)
295
     image = load_image(input_path)
276
 
296
 
280
     if unwarp:
300
     if unwarp:
281
         image = unwarp_image(image)
301
         image = unwarp_image(image)
282
 
302
 
303
+    if denoise:
304
+        image = denoise_image(image)
305
+
283
     if rows is None or cols is None:
306
     if rows is None or cols is None:
284
         cols, rows = detect_grid(image)
307
         cols, rows = detect_grid(image)
285
 
308
 

+ 1
- 1
tests/test_post_crop.py Zobrazit soubor

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

Načítá se…
Zrušit
Uložit