Преглед на файлове

image_prepare: --unwrap parameter

master
Evgeniy Ierusalimov преди 3 дни
родител
ревизия
d69314cfbc
променени са 4 файла, в които са добавени 70 реда и са изтрити 1 реда
  1. 6
    0
      README.md
  2. 14
    1
      src/image_prepare.py
  3. 2
    0
      src/split/__init__.py
  4. 48
    0
      src/split/slicer.py

+ 6
- 0
README.md Целия файл

@@ -62,12 +62,14 @@ src/image_ocr.py           ← Stage 2: OCR + Markdown
62 62
 | `--output-dir` / `-o` | Каталог для страниц | `.` |
63 63
 | `--pre-rotate` / `-r` | Поворот: 90, 180, 270 | — |
64 64
 | `--border` / `-b` | Белая рамка (px) | 50 |
65
+| `--unwarp` / `-u` | Выпрямить перспективные искажения (Canny → 4-угольный контур → warp) | OFF |
65 66
 | `--post-crop` / `--no-post-crop` | Обрезка по контенту | ON |
66 67
 
67 68
 Пример:
68 69
 ```bash
69 70
 python -m src.image_prepare -i scan.jpg -s 3:3 -b 10 -o pages/
70 71
 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
71 73
 ```
72 74
 
73 75
 <details>
@@ -105,6 +107,10 @@ python -m src.image_prepare -i scan.jpg --slice-auto
105 107
 │                                                         содержимого, по      │
106 108
 │                                                         умолчанию 50)        │
107 109
 │                                                         [default: 50]        │
110
+│ --unwarp      -u                                        Выпрямить            │
111
+│                                                         перспективные        │
112
+│                                                         искажения перед      │
113
+│                                                         разрезанием          │
108 114
 │ --post-crop   -c  --no-post-crop                        Обрезать каждую      │
109 115
 │                                                         страницу по границам │
110 116
 │                                                         полезного            │

+ 14
- 1
src/image_prepare.py Целия файл

@@ -16,6 +16,7 @@ from src.split.slicer import (
16 16
     parse_slice,
17 17
     process_image,
18 18
     rotate_image,
19
+    unwarp_image,
19 20
 )
20 21
 
21 22
 logging.basicConfig(
@@ -58,6 +59,7 @@ def _resolve_grid(
58 59
     slice_auto: bool,
59 60
     input_path: Path,
60 61
     pre_rotate: int | None,
62
+    unwarp: bool,
61 63
 ) -> tuple[int, int]:
62 64
     if slice_str is not None and slice_auto:
63 65
         raise typer.BadParameter(
@@ -70,6 +72,8 @@ def _resolve_grid(
70 72
         image = load_image(input_path)
71 73
         if pre_rotate is not None:
72 74
             image = rotate_image(image, pre_rotate)
75
+        if unwarp:
76
+            image = unwarp_image(image)
73 77
         cols, rows = detect_grid(image)
74 78
         typer.echo(f"Определена сетка: {cols}×{rows}")
75 79
         return cols, rows
@@ -135,6 +139,14 @@ def slice_pages(
135 139
             help="Отступ в пикселях (белая рамка / отступ при обрезке содержимого, по умолчанию 50)",
136 140
         ),
137 141
     ] = DEFAULT_BORDER_PX,
142
+    unwarp: Annotated[
143
+        bool,
144
+        typer.Option(
145
+            "--unwarp",
146
+            "-u",
147
+            help="Выпрямить перспективные искажения перед разрезанием",
148
+        ),
149
+    ] = False,
138 150
     post_crop: Annotated[
139 151
         bool,
140 152
         typer.Option(
@@ -147,7 +159,7 @@ def slice_pages(
147 159
     """Подготовить отсканированное изображение: разрезать по сетке, обрезать, добавить рамку."""
148 160
     input_path = _validate_input(input)
149 161
     _validate_rotation(pre_rotate)
150
-    cols, rows = _resolve_grid(slice, slice_auto, input_path, pre_rotate)
162
+    cols, rows = _resolve_grid(slice, slice_auto, input_path, pre_rotate, unwarp)
151 163
 
152 164
     output_paths = process_image(
153 165
         input_path=input_path,
@@ -157,6 +169,7 @@ def slice_pages(
157 169
         pre_rotate=pre_rotate,
158 170
         border_px=border,
159 171
         post_crop=post_crop,
172
+        unwarp=unwarp,
160 173
     )
161 174
 
162 175
     typer.echo(f"Сохранено {len(output_paths)} страниц в {output_dir.resolve()}")

+ 2
- 0
src/split/__init__.py Целия файл

@@ -6,6 +6,7 @@ from src.split.slicer import (
6 6
     parse_slice,
7 7
     process_image,
8 8
     rotate_image,
9
+    unwarp_image,
9 10
 )
10 11
 
11 12
 __all__ = [
@@ -16,4 +17,5 @@ __all__ = [
16 17
     "parse_slice",
17 18
     "process_image",
18 19
     "rotate_image",
20
+    "unwarp_image",
19 21
 ]

+ 48
- 0
src/split/slicer.py Целия файл

@@ -218,6 +218,50 @@ def crop_to_content(image: np.ndarray, border_px: int = DEFAULT_BORDER_PX) -> np
218 218
     return image[y1_crop:y2_crop, x1_crop:x2_crop]
219 219
 
220 220
 
221
+def _order_points(pts: np.ndarray) -> np.ndarray:
222
+    rect = np.zeros((4, 2), dtype=np.float32)
223
+    s = pts.sum(axis=1)
224
+    rect[0] = pts[np.argmin(s)]
225
+    rect[2] = pts[np.argmax(s)]
226
+    diff = np.diff(pts, axis=1)
227
+    rect[1] = pts[np.argmin(diff)]
228
+    rect[3] = pts[np.argmax(diff)]
229
+    return rect
230
+
231
+
232
+def _apply_perspective(image: np.ndarray, rect: np.ndarray) -> np.ndarray:
233
+    (tl, tr, br, bl) = rect
234
+    max_w = int(max(np.linalg.norm(br - bl), np.linalg.norm(tr - tl)))
235
+    max_h = int(max(np.linalg.norm(tr - br), np.linalg.norm(tl - bl)))
236
+    dst = np.array([[0, 0], [max_w - 1, 0], [max_w - 1, max_h - 1], [0, max_h - 1]], dtype=np.float32)
237
+    mtx = cv2.getPerspectiveTransform(rect, dst)
238
+    return cv2.warpPerspective(image, mtx, (max_w, max_h), flags=cv2.INTER_CUBIC)
239
+
240
+
241
+def unwarp_image(image: np.ndarray) -> np.ndarray:
242
+    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
243
+    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
244
+    edges = cv2.Canny(blurred, 50, 150)
245
+    contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
246
+    if not contours:
247
+        return image
248
+
249
+    h, w = image.shape[:2]
250
+    min_area = w * h * 0.3
251
+
252
+    contours = sorted(contours, key=cv2.contourArea, reverse=True)
253
+    for cnt in contours[:10]:
254
+        area = cv2.contourArea(cnt)
255
+        if area < min_area:
256
+            continue
257
+        peri = cv2.arcLength(cnt, True)
258
+        approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
259
+        if len(approx) == 4:
260
+            rect = _order_points(approx.reshape(4, 2))
261
+            return _apply_perspective(image, rect)
262
+    return image
263
+
264
+
221 265
 def process_image(
222 266
     input_path: Path,
223 267
     rows: int | None = None,
@@ -226,12 +270,16 @@ def process_image(
226 270
     pre_rotate: int | None = None,
227 271
     border_px: int = DEFAULT_BORDER_PX,
228 272
     post_crop: bool = False,
273
+    unwarp: bool = False,
229 274
 ) -> list[Path]:
230 275
     image = load_image(input_path)
231 276
 
232 277
     if pre_rotate is not None:
233 278
         image = rotate_image(image, pre_rotate)
234 279
 
280
+    if unwarp:
281
+        image = unwarp_image(image)
282
+
235 283
     if rows is None or cols is None:
236 284
         cols, rows = detect_grid(image)
237 285
 

Loading…
Отказ
Запис