Просмотр исходного кода

image_prepare: --unwrap parameter

master
Evgeniy Ierusalimov 3 дней назад
Родитель
Сommit
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
 | `--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) | 50 |
65
+| `--unwarp` / `-u` | Выпрямить перспективные искажения (Canny → 4-угольный контур → warp) | OFF |
65
 | `--post-crop` / `--no-post-crop` | Обрезка по контенту | ON |
66
 | `--post-crop` / `--no-post-crop` | Обрезка по контенту | ON |
66
 
67
 
67
 Пример:
68
 Пример:
68
 ```bash
69
 ```bash
69
 python -m src.image_prepare -i scan.jpg -s 3:3 -b 10 -o pages/
70
 python -m src.image_prepare -i scan.jpg -s 3:3 -b 10 -o pages/
70
 python -m src.image_prepare -i scan.jpg --slice-auto
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
 <details>
75
 <details>
105
 │                                                         содержимого, по      │
107
 │                                                         содержимого, по      │
106
 │                                                         умолчанию 50)        │
108
 │                                                         умолчанию 50)        │
107
 │                                                         [default: 50]        │
109
 │                                                         [default: 50]        │
110
+│ --unwarp      -u                                        Выпрямить            │
111
+│                                                         перспективные        │
112
+│                                                         искажения перед      │
113
+│                                                         разрезанием          │
108
 │ --post-crop   -c  --no-post-crop                        Обрезать каждую      │
114
 │ --post-crop   -c  --no-post-crop                        Обрезать каждую      │
109
 │                                                         страницу по границам │
115
 │                                                         страницу по границам │
110
 │                                                         полезного            │
116
 │                                                         полезного            │

+ 14
- 1
src/image_prepare.py Просмотреть файл

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

+ 2
- 0
src/split/__init__.py Просмотреть файл

6
     parse_slice,
6
     parse_slice,
7
     process_image,
7
     process_image,
8
     rotate_image,
8
     rotate_image,
9
+    unwarp_image,
9
 )
10
 )
10
 
11
 
11
 __all__ = [
12
 __all__ = [
16
     "parse_slice",
17
     "parse_slice",
17
     "process_image",
18
     "process_image",
18
     "rotate_image",
19
     "rotate_image",
20
+    "unwarp_image",
19
 ]
21
 ]

+ 48
- 0
src/split/slicer.py Просмотреть файл

218
     return image[y1_crop:y2_crop, x1_crop:x2_crop]
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
 def process_image(
265
 def process_image(
222
     input_path: Path,
266
     input_path: Path,
223
     rows: int | None = None,
267
     rows: int | None = None,
226
     pre_rotate: int | None = None,
270
     pre_rotate: int | None = None,
227
     border_px: int = DEFAULT_BORDER_PX,
271
     border_px: int = DEFAULT_BORDER_PX,
228
     post_crop: bool = False,
272
     post_crop: bool = False,
273
+    unwarp: bool = False,
229
 ) -> list[Path]:
274
 ) -> list[Path]:
230
     image = load_image(input_path)
275
     image = load_image(input_path)
231
 
276
 
232
     if pre_rotate is not None:
277
     if pre_rotate is not None:
233
         image = rotate_image(image, pre_rotate)
278
         image = rotate_image(image, pre_rotate)
234
 
279
 
280
+    if unwarp:
281
+        image = unwarp_image(image)
282
+
235
     if rows is None or cols is None:
283
     if rows is None or cols is None:
236
         cols, rows = detect_grid(image)
284
         cols, rows = detect_grid(image)
237
 
285
 

Загрузка…
Отмена
Сохранить