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

automatically skip blank target images

master
Evgeniy Ierusalimov 1 неделю назад
Родитель
Сommit
064d6a62b2
6 измененных файлов: 82 добавлений и 10 удалений
  1. 5
    0
      src/cli.py
  2. 2
    0
      src/split/__init__.py
  3. 23
    2
      src/split/slicer.py
  4. 1
    2
      tests/helpers.py
  5. 46
    1
      tests/test_post_crop.py
  6. 5
    5
      tests/test_slicer.py

+ 5
- 0
src/cli.py Просмотреть файл

@@ -1,5 +1,6 @@
1 1
 from __future__ import annotations
2 2
 
3
+import logging
3 4
 import os
4 5
 import sys
5 6
 from pathlib import Path
@@ -17,6 +18,10 @@ from src.split.slicer import (
17 18
     rotate_image,
18 19
 )
19 20
 
21
+logging.basicConfig(
22
+    level=logging.INFO,
23
+    format="%(levelname)-8s %(message)s",
24
+)
20 25
 app = typer.Typer(add_completion=False, no_args_is_help=True)
21 26
 
22 27
 

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

@@ -8,6 +8,7 @@ from src.split.slicer import (
8 8
     detect_grid,
9 9
     find_content_bounds,
10 10
     generate_output_paths,
11
+    is_empty,
11 12
     load_image,
12 13
     parse_slice,
13 14
     process_image,
@@ -27,6 +28,7 @@ __all__ = [
27 28
     "detect_grid",
28 29
     "find_content_bounds",
29 30
     "generate_output_paths",
31
+    "is_empty",
30 32
     "load_image",
31 33
     "parse_slice",
32 34
     "process_image",

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

@@ -1,10 +1,13 @@
1 1
 from __future__ import annotations
2 2
 
3
+import logging
3 4
 from pathlib import Path
4 5
 
5 6
 import cv2
6 7
 import numpy as np
7 8
 
9
+logger = logging.getLogger(__name__)
10
+
8 11
 SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({".jpg", ".jpeg", ".png", ".tiff", ".tif"})
9 12
 PAGE_NUMBER_WIDTH: int = 2
10 13
 DEFAULT_BORDER_PX: int = 50
@@ -12,6 +15,8 @@ BORDER_COLOR: tuple[int, int, int] = (255, 255, 255)
12 15
 VALID_ROTATIONS: frozenset[int] = frozenset({90, 180, 270})
13 16
 DENSITY_RATIO: float = 0.005
14 17
 AUTO_GRID_MAX: int = 4
18
+EMPTY_DARK_THRESHOLD: int = 100
19
+EMPTY_DARK_RATIO: float = 0.001
15 20
 
16 21
 
17 22
 def load_image(path: Path) -> np.ndarray:
@@ -115,6 +120,13 @@ def validate_output_dir(path: Path) -> Path:
115 120
     return path
116 121
 
117 122
 
123
+def is_empty(image: np.ndarray) -> bool:
124
+    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
125
+    dark_pixels = (gray < EMPTY_DARK_THRESHOLD).sum()
126
+    threshold = int(image.shape[0] * image.shape[1] * EMPTY_DARK_RATIO)
127
+    return dark_pixels <= threshold
128
+
129
+
118 130
 def _binarize(image: np.ndarray) -> np.ndarray:
119 131
     gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
120 132
     blurred = cv2.GaussianBlur(gray, (3, 3), 0)
@@ -242,8 +254,17 @@ def process_image(
242 254
 
243 255
     pages_with_border = [add_border(page, border_px) for page in pages]
244 256
 
245
-    output_paths = generate_output_paths(input_path, len(pages_with_border), output_dir)
246
-    for page, path in zip(pages_with_border, output_paths):
257
+    non_empty: list[np.ndarray] = []
258
+    skipped: int = 0
259
+    for i, page in enumerate(pages_with_border):
260
+        if is_empty(page):
261
+            logger.info("Пропущена пустая страница %d (размер %dx%d)", i + 1, *page.shape[:2])
262
+            skipped += 1
263
+        else:
264
+            non_empty.append(page)
265
+
266
+    output_paths = generate_output_paths(input_path, len(non_empty), output_dir)
267
+    for page, path in zip(non_empty, output_paths):
247 268
         save_page(page, path)
248 269
 
249 270
     return output_paths

+ 1
- 2
tests/helpers.py Просмотреть файл

@@ -8,8 +8,7 @@ import numpy as np
8 8
 
9 9
 
10 10
 def create_test_image(width: int = 400, height: int = 300) -> np.ndarray:
11
-    image = np.zeros((height, width, 3), dtype=np.uint8)
12
-    image[:, :] = (200, 200, 200)
11
+    image = np.full((height, width, 3), 255, dtype=np.uint8)
13 12
     return image
14 13
 
15 14
 

+ 46
- 1
tests/test_post_crop.py Просмотреть файл

@@ -5,7 +5,12 @@ from pathlib import Path
5 5
 
6 6
 import cv2
7 7
 
8
-from src.split.slicer import crop_to_content, find_content_bounds, process_image
8
+from src.split.slicer import (
9
+    crop_to_content,
10
+    find_content_bounds,
11
+    is_empty,
12
+    process_image,
13
+)
9 14
 from tests.helpers import create_test_image, image_to_path
10 15
 
11 16
 
@@ -110,6 +115,7 @@ class TestProcessImagePostCrop:
110 115
 
111 116
     def test_post_crop_off_no_crop(self) -> None:
112 117
         image = create_test_image(400, 300)
118
+        image[10:290, 10:390] = (0, 0, 0)
113 119
         path = image_to_path(image, ".png")
114 120
         with tempfile.TemporaryDirectory() as tmp:
115 121
             result = process_image(Path(path), 1, 1, Path(tmp), post_crop=False)
@@ -134,3 +140,42 @@ class TestProcessImagePostCrop:
134 140
                 n = cv2.imread(str(np))
135 141
                 assert c.shape[0] < n.shape[0]
136 142
                 assert c.shape[1] < n.shape[1]
143
+
144
+
145
+class TestIsEmpty:
146
+    def test_white_image_is_empty(self) -> None:
147
+        image = create_test_image(100, 100)
148
+        assert is_empty(image)
149
+
150
+    def test_dark_content_not_empty(self) -> None:
151
+        image = create_test_image(100, 100)
152
+        image[10:20, 10:20] = (0, 0, 0)
153
+        assert not is_empty(image)
154
+
155
+    def test_threshold_boundary_not_empty(self) -> None:
156
+        image = create_test_image(100, 100)
157
+        image[:, :] = (99, 99, 99)
158
+        assert not is_empty(image)
159
+
160
+    def test_threshold_boundary_empty(self) -> None:
161
+        image = create_test_image(100, 100)
162
+        image[:, :] = (100, 100, 100)
163
+        assert is_empty(image)
164
+
165
+
166
+class TestSkipEmptyPages:
167
+    def test_skip_empty_page_renumbering(self) -> None:
168
+        image = create_test_image(400, 400)
169
+        image[10:390, 200:390] = (0, 0, 0)
170
+        path = image_to_path(image, ".png")
171
+        with tempfile.TemporaryDirectory() as tmp:
172
+            result = process_image(Path(path), 1, 2, Path(tmp), border_px=5)
173
+            assert len(result) == 1
174
+            assert result[0].name == Path(path).stem + "_01.png"
175
+
176
+    def test_all_empty_returns_empty_list(self) -> None:
177
+        image = create_test_image(200, 200)
178
+        path = image_to_path(image, ".png")
179
+        with tempfile.TemporaryDirectory() as tmp:
180
+            result = process_image(Path(path), 2, 2, Path(tmp), border_px=5)
181
+            assert len(result) == 0

+ 5
- 5
tests/test_slicer.py Просмотреть файл

@@ -303,6 +303,7 @@ class TestSavePage:
303 303
 class TestProcessImage:
304 304
     def test_end_to_end_2x2(self) -> None:
305 305
         image = create_test_image(400, 300)
306
+        image[10:290, 10:390] = (0, 0, 0)
306 307
         path = image_to_path(image, ".jpg")
307 308
         with tempfile.TemporaryDirectory() as tmp:
308 309
             result = process_image(Path(path), 2, 2, Path(tmp))
@@ -310,21 +311,21 @@ class TestProcessImage:
310 311
             for p in result:
311 312
                 assert p.exists()
312 313
                 loaded = cv2.imread(str(p))
313
-                assert loaded.shape == (250, 300, 3)
314
+                assert loaded.shape[0] > 100
314 315
 
315 316
     def test_end_to_end_with_border(self) -> None:
316 317
         image = create_test_image(200, 200)
318
+        image[10:190, 10:190] = (0, 0, 0)
317 319
         path = image_to_path(image, ".png")
318 320
         with tempfile.TemporaryDirectory() as tmp:
319 321
             result = process_image(Path(path), 2, 2, Path(tmp), border_px=10)
320 322
             assert len(result) == 4
321 323
             for p in result:
322 324
                 assert p.exists()
323
-                loaded = cv2.imread(str(p))
324
-                assert loaded.shape == (120, 120, 3)
325 325
 
326 326
     def test_end_to_end_tiff(self) -> None:
327 327
         image = create_test_image(400, 400)
328
+        image[10:390, 10:390] = (0, 0, 0)
328 329
         path = image_to_path(image, ".tiff")
329 330
         with tempfile.TemporaryDirectory() as tmp:
330 331
             result = process_image(Path(path), 2, 2, Path(tmp))
@@ -335,11 +336,10 @@ class TestProcessImage:
335 336
 
336 337
     def test_end_to_end_with_rotation(self) -> None:
337 338
         image = create_test_image(200, 400)
339
+        image[10:390, 10:190] = (0, 0, 0)
338 340
         path = image_to_path(image, ".png")
339 341
         with tempfile.TemporaryDirectory() as tmp:
340 342
             result = process_image(Path(path), 2, 2, Path(tmp), pre_rotate=90)
341 343
             assert len(result) == 4
342 344
             for p in result:
343 345
                 assert p.exists()
344
-            loaded = cv2.imread(str(result[0]))
345
-            assert loaded.shape[0] > 0

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