Przeglądaj źródła

automatically skip blank target images

master
Evgeniy Ierusalimov 1 tydzień temu
rodzic
commit
064d6a62b2
6 zmienionych plików z 82 dodań i 10 usunięć
  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 Wyświetl plik

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

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

8
     detect_grid,
8
     detect_grid,
9
     find_content_bounds,
9
     find_content_bounds,
10
     generate_output_paths,
10
     generate_output_paths,
11
+    is_empty,
11
     load_image,
12
     load_image,
12
     parse_slice,
13
     parse_slice,
13
     process_image,
14
     process_image,
27
     "detect_grid",
28
     "detect_grid",
28
     "find_content_bounds",
29
     "find_content_bounds",
29
     "generate_output_paths",
30
     "generate_output_paths",
31
+    "is_empty",
30
     "load_image",
32
     "load_image",
31
     "parse_slice",
33
     "parse_slice",
32
     "process_image",
34
     "process_image",

+ 23
- 2
src/split/slicer.py Wyświetl plik

1
 from __future__ import annotations
1
 from __future__ import annotations
2
 
2
 
3
+import logging
3
 from pathlib import Path
4
 from pathlib import Path
4
 
5
 
5
 import cv2
6
 import cv2
6
 import numpy as np
7
 import numpy as np
7
 
8
 
9
+logger = logging.getLogger(__name__)
10
+
8
 SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({".jpg", ".jpeg", ".png", ".tiff", ".tif"})
11
 SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({".jpg", ".jpeg", ".png", ".tiff", ".tif"})
9
 PAGE_NUMBER_WIDTH: int = 2
12
 PAGE_NUMBER_WIDTH: int = 2
10
 DEFAULT_BORDER_PX: int = 50
13
 DEFAULT_BORDER_PX: int = 50
12
 VALID_ROTATIONS: frozenset[int] = frozenset({90, 180, 270})
15
 VALID_ROTATIONS: frozenset[int] = frozenset({90, 180, 270})
13
 DENSITY_RATIO: float = 0.005
16
 DENSITY_RATIO: float = 0.005
14
 AUTO_GRID_MAX: int = 4
17
 AUTO_GRID_MAX: int = 4
18
+EMPTY_DARK_THRESHOLD: int = 100
19
+EMPTY_DARK_RATIO: float = 0.001
15
 
20
 
16
 
21
 
17
 def load_image(path: Path) -> np.ndarray:
22
 def load_image(path: Path) -> np.ndarray:
115
     return path
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
 def _binarize(image: np.ndarray) -> np.ndarray:
130
 def _binarize(image: np.ndarray) -> np.ndarray:
119
     gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
131
     gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
120
     blurred = cv2.GaussianBlur(gray, (3, 3), 0)
132
     blurred = cv2.GaussianBlur(gray, (3, 3), 0)
242
 
254
 
243
     pages_with_border = [add_border(page, border_px) for page in pages]
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
         save_page(page, path)
268
         save_page(page, path)
248
 
269
 
249
     return output_paths
270
     return output_paths

+ 1
- 2
tests/helpers.py Wyświetl plik

8
 
8
 
9
 
9
 
10
 def create_test_image(width: int = 400, height: int = 300) -> np.ndarray:
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
     return image
12
     return image
14
 
13
 
15
 
14
 

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

5
 
5
 
6
 import cv2
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
 from tests.helpers import create_test_image, image_to_path
14
 from tests.helpers import create_test_image, image_to_path
10
 
15
 
11
 
16
 
110
 
115
 
111
     def test_post_crop_off_no_crop(self) -> None:
116
     def test_post_crop_off_no_crop(self) -> None:
112
         image = create_test_image(400, 300)
117
         image = create_test_image(400, 300)
118
+        image[10:290, 10:390] = (0, 0, 0)
113
         path = image_to_path(image, ".png")
119
         path = image_to_path(image, ".png")
114
         with tempfile.TemporaryDirectory() as tmp:
120
         with tempfile.TemporaryDirectory() as tmp:
115
             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)
134
                 n = cv2.imread(str(np))
140
                 n = cv2.imread(str(np))
135
                 assert c.shape[0] < n.shape[0]
141
                 assert c.shape[0] < n.shape[0]
136
                 assert c.shape[1] < n.shape[1]
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 Wyświetl plik

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

Ładowanie…
Anuluj
Zapisz