ImagePRO-Python 1.5.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/PKG-INFO +15 -1
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/pyproject.toml +1 -1
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/readme.md +14 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/__init__.py +5 -2
- imagepro_python-1.6.0/src/ImagePRO/batch/__init__.py +14 -0
- imagepro_python-1.6.0/src/ImagePRO/batch/batch.py +529 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO_Python.egg-info/PKG-INFO +15 -1
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO_Python.egg-info/SOURCES.txt +3 -0
- imagepro_python-1.6.0/tests/test_batch.py +378 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/LICENSE +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/setup.cfg +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/__init__.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/body_analysis/__init__.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/body_analysis/hand_tracking.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/face_analysis/__init__.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/face_analysis/face_comparison.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/face_analysis/face_detection.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/object_analysis/__init__.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/object_analysis/object_detection.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pipeline/__init__.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pipeline/pipeline.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/__init__.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/blur.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/contrast.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/crop.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/dataset_generator.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/grayscale.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/histogram.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/resize.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/rotate.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/pre_processing/sharpen.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/utils/__init__.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/utils/image.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO/utils/result.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO_Python.egg-info/dependency_links.txt +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO_Python.egg-info/requires.txt +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/src/ImagePRO_Python.egg-info/top_level.txt +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_blur.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_body_pose_estimation.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_contrast.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_crop.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_dataset_generator.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_eye_status_analysis.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_face_comparison.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_face_detection.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_face_mesh_analysis.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_grayscale.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_hand_tracking.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_head_pose_estimation.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_histogram.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_image.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_integration.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_object_detection.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_pipeline.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_resize.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_result.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_rotate.py +0 -0
- {imagepro_python-1.5.0 → imagepro_python-1.6.0}/tests/test_sharpen.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ImagePRO-Python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Professional & modular image processing library with OpenCV, MediaPipe, and YOLO integration.
|
|
5
5
|
Author-email: Parsa Safaie <parsasafaie.2568@proton.me>
|
|
6
6
|
License: MIT
|
|
@@ -81,6 +81,12 @@ Whether you're working on computer vision pipelines, preprocessing images for AI
|
|
|
81
81
|
- **Flexible Steps**: Any ImagePRO function or custom callable; per-step keyword arguments via `Step`
|
|
82
82
|
- **Pipeline Metadata**: Every result lists the executed steps and each step's parameters
|
|
83
83
|
|
|
84
|
+
### **Batch Processing**
|
|
85
|
+
- **Many Images at Once**: Run any function, `Step`, or `Pipeline` over a folder, a glob pattern, or a list of images
|
|
86
|
+
- **Ordered Results**: One result per input, in input order, with per-input failure reporting
|
|
87
|
+
- **Optional Output Saving**: Mirror results into an output directory with collision-safe names
|
|
88
|
+
- **Progress Reporting**: Optional progress callback; optional worker threads
|
|
89
|
+
|
|
84
90
|
## Installation
|
|
85
91
|
|
|
86
92
|
### From PyPI
|
|
@@ -175,6 +181,12 @@ pipeline = Pipeline([
|
|
|
175
181
|
pipeline_result = pipeline.run("person_and_objects.jpg")
|
|
176
182
|
pipeline_result.save_as_img("processed_output.jpg")
|
|
177
183
|
print(pipeline_result.meta["steps"]) # Executed step names
|
|
184
|
+
|
|
185
|
+
# Or run the same pipeline over a whole folder at once
|
|
186
|
+
from ImagePRO.batch import batch_process
|
|
187
|
+
|
|
188
|
+
batch = batch_process("photos/", pipeline, output_dir="processed/")
|
|
189
|
+
print(batch.meta["succeeded"], "images processed")
|
|
178
190
|
```
|
|
179
191
|
|
|
180
192
|
> **Note**: These are basic examples. Each module contains many more functions with extensive customization options. Explore the module-specific README files for detailed documentation.
|
|
@@ -189,6 +201,7 @@ Each module includes comprehensive documentation with detailed examples:
|
|
|
189
201
|
- [Body Analysis](src/ImagePRO/human_analysis/body_analysis/README.md): Body pose and hand tracking
|
|
190
202
|
- **[Object Analysis](src/ImagePRO/object_analysis/README.md)**: YOLO-based object detection
|
|
191
203
|
- **[Pipeline](src/ImagePRO/pipeline/README.md)**: Reusable chains of image processing operations
|
|
204
|
+
- **[Batch Processing](src/ImagePRO/batch/README.md)**: Run any operation over many images at once
|
|
192
205
|
- **[Utils](src/ImagePRO/utils/README.md)**: Shared utilities and I/O handling
|
|
193
206
|
|
|
194
207
|
For detailed project structure and development guidelines, see [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md).
|
|
@@ -201,6 +214,7 @@ ImagePRO is built with a modular architecture designed for extensibility and mai
|
|
|
201
214
|
- **Consistent API Patterns**: All functions follow the same input/output conventions
|
|
202
215
|
- **Shared Utilities**: Common `Image` and `Result` classes for unified I/O
|
|
203
216
|
- **Reusable Pipelines**: Chain any operations into a `Pipeline` and run it on any input
|
|
217
|
+
- **Batch Processing**: Run any operation or pipeline over many images at once, with ordered per-input results
|
|
204
218
|
- **Lazy Optional Imports**: Heavy AI dependencies (MediaPipe, Ultralytics, InsightFace) are only imported inside the functions that use them, keeping `import ImagePRO` fast and independent of installed extras
|
|
205
219
|
- **Professional Error Handling**: Comprehensive validation with clear error messages
|
|
206
220
|
- **Type Safety**: Full type hints throughout the codebase
|
|
@@ -7,7 +7,7 @@ where = ["src"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "ImagePRO-Python"
|
|
10
|
-
version = "1.
|
|
10
|
+
version = "1.6.0"
|
|
11
11
|
description = "Professional & modular image processing library with OpenCV, MediaPipe, and YOLO integration."
|
|
12
12
|
readme = "readme.md"
|
|
13
13
|
requires-python = ">=3.9,<3.13"
|
|
@@ -32,6 +32,12 @@ Whether you're working on computer vision pipelines, preprocessing images for AI
|
|
|
32
32
|
- **Flexible Steps**: Any ImagePRO function or custom callable; per-step keyword arguments via `Step`
|
|
33
33
|
- **Pipeline Metadata**: Every result lists the executed steps and each step's parameters
|
|
34
34
|
|
|
35
|
+
### **Batch Processing**
|
|
36
|
+
- **Many Images at Once**: Run any function, `Step`, or `Pipeline` over a folder, a glob pattern, or a list of images
|
|
37
|
+
- **Ordered Results**: One result per input, in input order, with per-input failure reporting
|
|
38
|
+
- **Optional Output Saving**: Mirror results into an output directory with collision-safe names
|
|
39
|
+
- **Progress Reporting**: Optional progress callback; optional worker threads
|
|
40
|
+
|
|
35
41
|
## Installation
|
|
36
42
|
|
|
37
43
|
### From PyPI
|
|
@@ -126,6 +132,12 @@ pipeline = Pipeline([
|
|
|
126
132
|
pipeline_result = pipeline.run("person_and_objects.jpg")
|
|
127
133
|
pipeline_result.save_as_img("processed_output.jpg")
|
|
128
134
|
print(pipeline_result.meta["steps"]) # Executed step names
|
|
135
|
+
|
|
136
|
+
# Or run the same pipeline over a whole folder at once
|
|
137
|
+
from ImagePRO.batch import batch_process
|
|
138
|
+
|
|
139
|
+
batch = batch_process("photos/", pipeline, output_dir="processed/")
|
|
140
|
+
print(batch.meta["succeeded"], "images processed")
|
|
129
141
|
```
|
|
130
142
|
|
|
131
143
|
> **Note**: These are basic examples. Each module contains many more functions with extensive customization options. Explore the module-specific README files for detailed documentation.
|
|
@@ -140,6 +152,7 @@ Each module includes comprehensive documentation with detailed examples:
|
|
|
140
152
|
- [Body Analysis](src/ImagePRO/human_analysis/body_analysis/README.md): Body pose and hand tracking
|
|
141
153
|
- **[Object Analysis](src/ImagePRO/object_analysis/README.md)**: YOLO-based object detection
|
|
142
154
|
- **[Pipeline](src/ImagePRO/pipeline/README.md)**: Reusable chains of image processing operations
|
|
155
|
+
- **[Batch Processing](src/ImagePRO/batch/README.md)**: Run any operation over many images at once
|
|
143
156
|
- **[Utils](src/ImagePRO/utils/README.md)**: Shared utilities and I/O handling
|
|
144
157
|
|
|
145
158
|
For detailed project structure and development guidelines, see [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md).
|
|
@@ -152,6 +165,7 @@ ImagePRO is built with a modular architecture designed for extensibility and mai
|
|
|
152
165
|
- **Consistent API Patterns**: All functions follow the same input/output conventions
|
|
153
166
|
- **Shared Utilities**: Common `Image` and `Result` classes for unified I/O
|
|
154
167
|
- **Reusable Pipelines**: Chain any operations into a `Pipeline` and run it on any input
|
|
168
|
+
- **Batch Processing**: Run any operation or pipeline over many images at once, with ordered per-input results
|
|
155
169
|
- **Lazy Optional Imports**: Heavy AI dependencies (MediaPipe, Ultralytics, InsightFace) are only imported inside the functions that use them, keeping `import ImagePRO` fast and independent of installed extras
|
|
156
170
|
- **Professional Error Handling**: Comprehensive validation with clear error messages
|
|
157
171
|
- **Type Safety**: Full type hints throughout the codebase
|
|
@@ -13,10 +13,11 @@ Features:
|
|
|
13
13
|
- Human analysis: face mesh, pose estimation, hand tracking
|
|
14
14
|
- Object detection with YOLO models
|
|
15
15
|
- Reusable pipelines for chaining operations
|
|
16
|
+
- Batch processing: run any operation over many images at once
|
|
16
17
|
- Real-time processing capabilities
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
__version__ = "1.
|
|
20
|
+
__version__ = "1.6.0"
|
|
20
21
|
__author__ = "Parsa Safaie"
|
|
21
22
|
__email__ = "parsasafaie.2568@proton.me"
|
|
22
23
|
|
|
@@ -26,11 +27,13 @@ from . import pre_processing
|
|
|
26
27
|
from . import human_analysis
|
|
27
28
|
from . import object_analysis
|
|
28
29
|
from . import pipeline
|
|
30
|
+
from . import batch
|
|
29
31
|
|
|
30
32
|
__all__ = [
|
|
31
33
|
"utils",
|
|
32
34
|
"pre_processing",
|
|
33
35
|
"human_analysis",
|
|
34
36
|
"object_analysis",
|
|
35
|
-
"pipeline"
|
|
37
|
+
"pipeline",
|
|
38
|
+
"batch"
|
|
36
39
|
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Batch Module
|
|
2
|
+
# Runs a single operation over many inputs at once
|
|
3
|
+
#
|
|
4
|
+
# Uses only base dependencies (numpy); the operation handed to
|
|
5
|
+
# batch_process may come from any module, including those with optional
|
|
6
|
+
# backends (mediapipe, ultralytics, insightface).
|
|
7
|
+
|
|
8
|
+
from .batch import BatchFailure, BatchResult, batch_process
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BatchFailure",
|
|
12
|
+
"BatchResult",
|
|
13
|
+
"batch_process"
|
|
14
|
+
]
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""Batch processing for ImagePRO: run one operation over many images.
|
|
2
|
+
|
|
3
|
+
Exposes ``batch_process`` (the runner), ``BatchResult`` (the aggregate
|
|
4
|
+
return value), and ``BatchFailure`` (one input that could not be
|
|
5
|
+
processed). The module itself needs only the base dependencies; the
|
|
6
|
+
operation handed to ``batch_process`` may come from any module, including
|
|
7
|
+
those with optional backends.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import glob
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Optional, Union
|
|
19
|
+
|
|
20
|
+
# Add src directory to path for absolute imports
|
|
21
|
+
_file_path = Path(__file__).resolve()
|
|
22
|
+
_src_path = _file_path.parents[2] # Go up to src directory
|
|
23
|
+
if str(_src_path) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_src_path))
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from ImagePRO.pipeline import Pipeline, Step
|
|
29
|
+
from ImagePRO.utils.image import Image
|
|
30
|
+
from ImagePRO.utils.result import Result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Constants
|
|
34
|
+
IMAGE_EXTENSIONS = frozenset({
|
|
35
|
+
".bmp", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp",
|
|
36
|
+
})
|
|
37
|
+
GLOB_MAGIC_CHARS = "*?["
|
|
38
|
+
ON_ERROR_MODES = ("raise", "collect")
|
|
39
|
+
DEFAULT_ON_ERROR = "raise"
|
|
40
|
+
DEFAULT_WORKERS = 1
|
|
41
|
+
DEFAULT_RECURSIVE = False
|
|
42
|
+
|
|
43
|
+
InputLike = Union[Image, np.ndarray, str, Path]
|
|
44
|
+
InputsLike = Union[InputLike, list[InputLike], tuple[InputLike, ...]]
|
|
45
|
+
Operation = Callable[..., Result]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, eq=False)
|
|
49
|
+
class BatchFailure:
|
|
50
|
+
"""
|
|
51
|
+
One input that could not be processed.
|
|
52
|
+
|
|
53
|
+
Only produced when ``batch_process`` runs with ``on_error="collect"``;
|
|
54
|
+
the other inputs keep processing and the batch still returns.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
index (int):
|
|
58
|
+
Position of the failed input in the original input list (or in
|
|
59
|
+
the directory/glob listing order).
|
|
60
|
+
source (Any):
|
|
61
|
+
The original input item: a file path (``pathlib.Path``), an
|
|
62
|
+
``Image``, or a ``numpy.ndarray``.
|
|
63
|
+
error_type (str):
|
|
64
|
+
Name of the exception class (e.g. "ValueError").
|
|
65
|
+
error (str):
|
|
66
|
+
The exception message.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
index: int
|
|
70
|
+
source: Any
|
|
71
|
+
error_type: str
|
|
72
|
+
error: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class BatchResult:
|
|
77
|
+
"""
|
|
78
|
+
Aggregate result of a batch run.
|
|
79
|
+
|
|
80
|
+
Holds one entry per input, in input order, plus the batch-level
|
|
81
|
+
metadata (counts, configuration, timing).
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
results (list[Optional[Result]]):
|
|
85
|
+
One slot per input, in input order. Successful inputs hold
|
|
86
|
+
their ``Result`` object (exactly what the operation returned
|
|
87
|
+
for that image). Slots are ``None`` for failed inputs — only
|
|
88
|
+
possible with ``on_error="collect"``, since the default
|
|
89
|
+
"raise" mode aborts the batch on the first failure.
|
|
90
|
+
failures (list[BatchFailure]):
|
|
91
|
+
Failed inputs, ordered by index. Empty unless
|
|
92
|
+
``on_error="collect"``.
|
|
93
|
+
meta (dict[str, Any]):
|
|
94
|
+
Batch metadata: "operation", "total", "succeeded", "failed",
|
|
95
|
+
"on_error", "workers", "recursive", "duration_seconds", and
|
|
96
|
+
"output_dir" (``None`` when results were kept in memory only).
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
results: list[Optional[Result]] = field(default_factory=list)
|
|
100
|
+
failures: list[BatchFailure] = field(default_factory=list)
|
|
101
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def batch_process(
|
|
105
|
+
inputs: InputsLike,
|
|
106
|
+
operation: Operation,
|
|
107
|
+
*,
|
|
108
|
+
on_error: str = DEFAULT_ON_ERROR,
|
|
109
|
+
output_dir: Optional[str | Path] = None,
|
|
110
|
+
recursive: bool = DEFAULT_RECURSIVE,
|
|
111
|
+
workers: int = DEFAULT_WORKERS,
|
|
112
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
113
|
+
**kwargs: Any,
|
|
114
|
+
) -> BatchResult:
|
|
115
|
+
"""Run one operation over many images and collect the results.
|
|
116
|
+
|
|
117
|
+
Collects every input — a directory, a glob pattern, or a list of file
|
|
118
|
+
paths, Image instances and NumPy arrays — then runs ``operation`` on
|
|
119
|
+
each one, in input order. Inputs are loaded one at a time, so a folder
|
|
120
|
+
with thousands of images never has to fit in memory at once.
|
|
121
|
+
|
|
122
|
+
The operation can be any ImagePRO function (which takes an ``Image``
|
|
123
|
+
and returns a ``Result``), a ``Step``, a ``Pipeline``, or any custom
|
|
124
|
+
callable following the same convention. Returning a plain
|
|
125
|
+
``numpy.ndarray`` is accepted and wrapped in a ``Result``. Keyword
|
|
126
|
+
arguments given here are forwarded to the operation on every input,
|
|
127
|
+
exactly like ``Step``.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
inputs (list | tuple | str | Path | Image | np.ndarray):
|
|
131
|
+
What to process. Accepts:
|
|
132
|
+
- A directory path: every image file inside it (case-
|
|
133
|
+
insensitive extension match against ``.bmp .jpeg .jpg .png
|
|
134
|
+
.tif .tiff .webp``), sorted by path. Sub-directories are
|
|
135
|
+
included only with ``recursive=True``.
|
|
136
|
+
- A glob pattern (containing ``*``, ``?`` or ``[``): matching
|
|
137
|
+
image files, sorted by path.
|
|
138
|
+
- A single file path, ``Image``, or NumPy array: that one
|
|
139
|
+
input.
|
|
140
|
+
- A list/tuple mixing file paths, ``Image`` instances and
|
|
141
|
+
NumPy arrays.
|
|
142
|
+
operation (Callable):
|
|
143
|
+
The callable to run on each input. Pre-loaded detectors can be
|
|
144
|
+
injected through it — e.g. ``Step(detect_objects, model=my_model)``
|
|
145
|
+
— which matters a lot for heavy backends: without a pre-loaded
|
|
146
|
+
model, a fresh detector is constructed for every image.
|
|
147
|
+
on_error (str, optional):
|
|
148
|
+
What to do when an input fails. "raise" (default) re-raises
|
|
149
|
+
the operation's own error unchanged, like every other ImagePRO
|
|
150
|
+
function. "collect" keeps processing the remaining inputs and
|
|
151
|
+
records the failure in ``BatchResult.failures``. Either way,
|
|
152
|
+
an ``ImportError`` for a missing optional backend always
|
|
153
|
+
propagates: it is a setup problem, not a bad input.
|
|
154
|
+
output_dir (str | Path, optional):
|
|
155
|
+
If given, each successful result's image is saved there: the
|
|
156
|
+
output name mirrors the input name (relative sub-paths are
|
|
157
|
+
preserved for directory inputs) and name clashes get a numeric
|
|
158
|
+
suffix (``a.png``, ``a_1.png``, ...). Results without an image
|
|
159
|
+
(data-only results) are not saved. Defaults to None (keep
|
|
160
|
+
results in memory only).
|
|
161
|
+
recursive (bool, optional):
|
|
162
|
+
Include sub-directories when ``inputs`` is a directory.
|
|
163
|
+
Defaults to False.
|
|
164
|
+
workers (int, optional):
|
|
165
|
+
Number of worker threads. Defaults to 1 (fully sequential,
|
|
166
|
+
deterministic order). With more than one, inputs are processed
|
|
167
|
+
concurrently but ``BatchResult.results`` still follows input
|
|
168
|
+
order. Detector objects injected into the operation (MediaPipe,
|
|
169
|
+
YOLO, InsightFace) are not guaranteed thread-safe: keep
|
|
170
|
+
``workers=1`` unless the operation is pure image processing
|
|
171
|
+
(e.g. OpenCV filters, which release the GIL).
|
|
172
|
+
progress_callback (Callable[[int, int], None], optional):
|
|
173
|
+
Called after each input completes with ``(completed, total)``.
|
|
174
|
+
Always called from the calling thread, never from a worker.
|
|
175
|
+
Defaults to None.
|
|
176
|
+
**kwargs (Any):
|
|
177
|
+
Forwarded to the operation on every input, like ``Step``.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
BatchResult: One entry per input (``results``, input order),
|
|
181
|
+
``failures`` (empty in "raise" mode), and batch ``meta``.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
TypeError: If ``inputs``, ``operation``, ``output_dir``,
|
|
185
|
+
``progress_callback`` or ``recursive`` has an invalid type.
|
|
186
|
+
ValueError: If ``on_error`` or ``workers`` is invalid, if
|
|
187
|
+
``output_dir`` points at an existing non-directory, or if
|
|
188
|
+
keyword arguments are combined with a ``Step``/``Pipeline``
|
|
189
|
+
operation (attach them to the Step instead).
|
|
190
|
+
FileNotFoundError: If a single input path does not exist.
|
|
191
|
+
TypeError/ValueError: Propagated unchanged from the operation or
|
|
192
|
+
from input normalization when ``on_error="raise"``.
|
|
193
|
+
|
|
194
|
+
Note:
|
|
195
|
+
The batch options (``on_error``, ``output_dir``, ``recursive``,
|
|
196
|
+
``workers``, ``progress_callback``) are reserved: to pass a
|
|
197
|
+
parameter with one of these names to the operation itself, wrap
|
|
198
|
+
the operation in a ``Step`` or ``functools.partial`` instead of
|
|
199
|
+
passing it here.
|
|
200
|
+
|
|
201
|
+
Inputs are read one at a time, but the returned ``BatchResult``
|
|
202
|
+
keeps every result, and each result keeps its input in
|
|
203
|
+
``meta["source"]``. A batch therefore holds about the whole
|
|
204
|
+
dataset (inputs plus outputs) in memory; ``output_dir`` writes
|
|
205
|
+
images to disk without releasing them. For very large inputs, call
|
|
206
|
+
``batch_process`` once per chunk of the input list so each
|
|
207
|
+
``BatchResult`` can be dropped before the next one is built.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> from ImagePRO.pre_processing.blur import apply_gaussian_blur
|
|
211
|
+
>>> batch = batch_process("photos/", apply_gaussian_blur, kernel_size=(9, 9))
|
|
212
|
+
>>> batch.meta["succeeded"]
|
|
213
|
+
2
|
|
214
|
+
>>> batch.results[0].save_as_img("first_blurred.png")
|
|
215
|
+
"""
|
|
216
|
+
# Validate the batch configuration before touching any input, so an
|
|
217
|
+
# invalid call never leaves partial state (e.g. a created folder)
|
|
218
|
+
# behind.
|
|
219
|
+
if not callable(operation):
|
|
220
|
+
raise TypeError(
|
|
221
|
+
"'operation' must be a callable that accepts an Image as its "
|
|
222
|
+
"first positional argument (an ImagePRO function, a Step, or a "
|
|
223
|
+
"Pipeline)."
|
|
224
|
+
)
|
|
225
|
+
if on_error not in ON_ERROR_MODES:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"'on_error' must be one of {list(ON_ERROR_MODES)}, got {on_error!r}."
|
|
228
|
+
)
|
|
229
|
+
if not isinstance(workers, int) or isinstance(workers, bool) or workers < 1:
|
|
230
|
+
raise ValueError("'workers' must be a positive integer.")
|
|
231
|
+
if not isinstance(recursive, bool):
|
|
232
|
+
raise TypeError("'recursive' must be a boolean.")
|
|
233
|
+
if output_dir is not None:
|
|
234
|
+
if not isinstance(output_dir, (str, Path)):
|
|
235
|
+
raise TypeError("'output_dir' must be a string or pathlib.Path.")
|
|
236
|
+
out_root = Path(output_dir)
|
|
237
|
+
if out_root.exists() and not out_root.is_dir():
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"'output_dir' exists and is not a directory: {out_root}"
|
|
240
|
+
)
|
|
241
|
+
out_root.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
else:
|
|
243
|
+
out_root = None
|
|
244
|
+
if progress_callback is not None and not callable(progress_callback):
|
|
245
|
+
raise TypeError("'progress_callback' must be a callable.")
|
|
246
|
+
if kwargs and isinstance(operation, (Step, Pipeline)):
|
|
247
|
+
raise ValueError(
|
|
248
|
+
"Keyword arguments cannot be combined with a Step or Pipeline "
|
|
249
|
+
"operation; their keyword arguments are already fixed at "
|
|
250
|
+
"construction time. Attach them to the Step instead, e.g. "
|
|
251
|
+
"Step(resize_image, new_size=(800, 600))."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Collect all inputs up front; paths are only read during processing,
|
|
255
|
+
# one at a time. Output paths are pre-assigned here (rather than per
|
|
256
|
+
# worker) so collision suffixes stay deterministic and thread-safe.
|
|
257
|
+
sources = _collect_sources(inputs, recursive=recursive)
|
|
258
|
+
total = len(sources)
|
|
259
|
+
taken_names: set[Path] = set()
|
|
260
|
+
if out_root is not None:
|
|
261
|
+
output_paths = [
|
|
262
|
+
_assign_output_path(out_root, name, taken_names)
|
|
263
|
+
for _, name in sources
|
|
264
|
+
]
|
|
265
|
+
else:
|
|
266
|
+
output_paths = [None] * total
|
|
267
|
+
|
|
268
|
+
operation_name = getattr(operation, "__name__", type(operation).__name__)
|
|
269
|
+
|
|
270
|
+
def run_item(index: int) -> tuple[int, Optional[Result], Optional[Exception]]:
|
|
271
|
+
"""Process one input; returns (index, result, error)."""
|
|
272
|
+
source = sources[index][0]
|
|
273
|
+
out_path = output_paths[index]
|
|
274
|
+
try:
|
|
275
|
+
image = _as_image(source)
|
|
276
|
+
output = operation(image, **kwargs)
|
|
277
|
+
if isinstance(output, Result):
|
|
278
|
+
result = output
|
|
279
|
+
elif isinstance(output, np.ndarray):
|
|
280
|
+
result = Result(image=output, meta={"operation": operation_name})
|
|
281
|
+
else:
|
|
282
|
+
raise TypeError(
|
|
283
|
+
f"The operation returned {type(output).__name__}; batch "
|
|
284
|
+
"operations must return a Result object or a numpy.ndarray."
|
|
285
|
+
)
|
|
286
|
+
if out_path is not None and result.image is not None:
|
|
287
|
+
result.save_as_img(out_path)
|
|
288
|
+
return index, result, None
|
|
289
|
+
except ImportError:
|
|
290
|
+
# A missing optional backend is a setup problem, not a bad
|
|
291
|
+
# input: collect mode must not swallow it N times over.
|
|
292
|
+
raise
|
|
293
|
+
except Exception as exc:
|
|
294
|
+
return index, None, exc
|
|
295
|
+
|
|
296
|
+
results: list[Optional[Result]] = [None] * total
|
|
297
|
+
failures: list[BatchFailure] = []
|
|
298
|
+
completed = 0
|
|
299
|
+
started = time.monotonic()
|
|
300
|
+
|
|
301
|
+
def record_item(index: int, result: Optional[Result], error: Optional[Exception]) -> None:
|
|
302
|
+
"""Record one finished item and report progress from this thread."""
|
|
303
|
+
nonlocal completed
|
|
304
|
+
if error is not None:
|
|
305
|
+
failures.append(BatchFailure(
|
|
306
|
+
index=index,
|
|
307
|
+
source=sources[index][0],
|
|
308
|
+
error_type=type(error).__name__,
|
|
309
|
+
error=str(error),
|
|
310
|
+
))
|
|
311
|
+
else:
|
|
312
|
+
results[index] = result
|
|
313
|
+
completed += 1
|
|
314
|
+
if progress_callback is not None:
|
|
315
|
+
progress_callback(completed, total)
|
|
316
|
+
|
|
317
|
+
if on_error == "raise":
|
|
318
|
+
if workers > 1 and total > 1:
|
|
319
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
320
|
+
futures = {executor.submit(run_item, index): index for index in range(total)}
|
|
321
|
+
try:
|
|
322
|
+
for future in as_completed(futures):
|
|
323
|
+
index, result, error = future.result()
|
|
324
|
+
if error is not None:
|
|
325
|
+
raise error
|
|
326
|
+
record_item(index, result, None)
|
|
327
|
+
finally:
|
|
328
|
+
for future in futures:
|
|
329
|
+
future.cancel()
|
|
330
|
+
else:
|
|
331
|
+
for index in range(total):
|
|
332
|
+
_, result, error = run_item(index)
|
|
333
|
+
if error is not None:
|
|
334
|
+
raise error
|
|
335
|
+
record_item(index, result, None)
|
|
336
|
+
else: # on_error == "collect"
|
|
337
|
+
if workers > 1 and total > 1:
|
|
338
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
339
|
+
futures = {executor.submit(run_item, index): index for index in range(total)}
|
|
340
|
+
for future in as_completed(futures):
|
|
341
|
+
index, result, error = future.result()
|
|
342
|
+
record_item(index, result, error)
|
|
343
|
+
else:
|
|
344
|
+
for index in range(total):
|
|
345
|
+
_, result, error = run_item(index)
|
|
346
|
+
record_item(index, result, error)
|
|
347
|
+
|
|
348
|
+
# as_completed order is not deterministic; sort for reproducible
|
|
349
|
+
# failure listings regardless of the worker count.
|
|
350
|
+
failures.sort(key=lambda failure: failure.index)
|
|
351
|
+
|
|
352
|
+
meta = {
|
|
353
|
+
"operation": operation_name,
|
|
354
|
+
"total": total,
|
|
355
|
+
"succeeded": sum(1 for result in results if result is not None),
|
|
356
|
+
"failed": len(failures),
|
|
357
|
+
"on_error": on_error,
|
|
358
|
+
"workers": workers,
|
|
359
|
+
"recursive": recursive,
|
|
360
|
+
"duration_seconds": round(time.monotonic() - started, 6),
|
|
361
|
+
"output_dir": str(out_root) if out_root is not None else None,
|
|
362
|
+
}
|
|
363
|
+
return BatchResult(results=results, failures=failures, meta=meta)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _as_image(source: Any) -> Image:
|
|
367
|
+
"""
|
|
368
|
+
Normalize one collected input into an Image.
|
|
369
|
+
|
|
370
|
+
Mirrors the input normalization of ``Pipeline.run``: file paths are
|
|
371
|
+
loaded from disk and NumPy arrays are wrapped, assuming BGR for
|
|
372
|
+
3-channel and GRAY for 2D arrays.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
source (Any): A ``pathlib.Path``, ``Image``, or NumPy array.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Image: The normalized input.
|
|
379
|
+
"""
|
|
380
|
+
if isinstance(source, Image):
|
|
381
|
+
return source
|
|
382
|
+
if isinstance(source, np.ndarray):
|
|
383
|
+
colorspace = "GRAY" if source.ndim == 2 else "BGR"
|
|
384
|
+
return Image.from_array(source, colorspace=colorspace)
|
|
385
|
+
return Image.from_path(source)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _collect_sources(inputs: InputsLike, *, recursive: bool) -> list[tuple[Any, Any]]:
|
|
389
|
+
"""
|
|
390
|
+
Turn the ``inputs`` argument into (source, output name) pairs.
|
|
391
|
+
|
|
392
|
+
Every pair is one work item: ``source`` is a file path, an ``Image``
|
|
393
|
+
or a NumPy array handed to the operation; ``output name`` is the name
|
|
394
|
+
used for saving under ``output_dir`` (a relative path for directory
|
|
395
|
+
inputs, a file name for list inputs, ``image_<index>`` for arrays).
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
inputs (InputsLike): The raw ``inputs`` argument.
|
|
399
|
+
recursive (bool): Whether directory scans descend into
|
|
400
|
+
sub-directories.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
list[tuple[Any, Any]]: One (source, output name) pair per input,
|
|
404
|
+
in input order.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
TypeError: If ``inputs`` or one of its items has an invalid type.
|
|
408
|
+
FileNotFoundError: If a single input path does not exist.
|
|
409
|
+
"""
|
|
410
|
+
if isinstance(inputs, (Image, np.ndarray)):
|
|
411
|
+
return [(inputs, "image_0")]
|
|
412
|
+
if isinstance(inputs, (list, tuple)):
|
|
413
|
+
sources: list[tuple[Any, Any]] = []
|
|
414
|
+
for index, item in enumerate(inputs):
|
|
415
|
+
if isinstance(item, (str, Path)):
|
|
416
|
+
sources.append((Path(item), Path(item).name))
|
|
417
|
+
elif isinstance(item, (Image, np.ndarray)):
|
|
418
|
+
sources.append((item, f"image_{index}"))
|
|
419
|
+
else:
|
|
420
|
+
raise TypeError(
|
|
421
|
+
"Every input must be a file path (str or pathlib.Path), "
|
|
422
|
+
"an Image instance, or a numpy.ndarray; got "
|
|
423
|
+
f"{type(item).__name__} at index {index}."
|
|
424
|
+
)
|
|
425
|
+
return sources
|
|
426
|
+
if isinstance(inputs, (str, Path)):
|
|
427
|
+
return _collect_from_path(inputs, recursive=recursive)
|
|
428
|
+
raise TypeError(
|
|
429
|
+
"'inputs' must be a list/tuple of images, a directory path, a glob "
|
|
430
|
+
"pattern, or a single Image or numpy.ndarray; got "
|
|
431
|
+
f"{type(inputs).__name__}."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _collect_from_path(value: str | Path, *, recursive: bool) -> list[tuple[Path, Any]]:
|
|
436
|
+
"""
|
|
437
|
+
Expand a single string/path input into its work items.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
value (str | Path): A directory path, a glob pattern, or a file
|
|
441
|
+
path.
|
|
442
|
+
recursive (bool): Whether directory scans descend into
|
|
443
|
+
sub-directories.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
list[tuple[Path, Any]]: One (source, output name) pair per match,
|
|
447
|
+
sorted by path.
|
|
448
|
+
|
|
449
|
+
Raises:
|
|
450
|
+
FileNotFoundError: If the path is neither a glob pattern nor an
|
|
451
|
+
existing file or directory.
|
|
452
|
+
"""
|
|
453
|
+
if any(char in str(value) for char in GLOB_MAGIC_CHARS):
|
|
454
|
+
return [(match, match.name) for match in _expand_glob(str(value))]
|
|
455
|
+
path = Path(value)
|
|
456
|
+
if path.is_dir():
|
|
457
|
+
return [
|
|
458
|
+
(match, match.relative_to(path))
|
|
459
|
+
for match in _scan_directory(path, recursive=recursive)
|
|
460
|
+
]
|
|
461
|
+
if path.is_file():
|
|
462
|
+
return [(path, path.name)]
|
|
463
|
+
raise FileNotFoundError(f"Input path does not exist: {path}")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _scan_directory(root: Path, *, recursive: bool) -> list[Path]:
|
|
467
|
+
"""
|
|
468
|
+
List the image files inside a directory, sorted by path.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
root (Path): Directory to scan.
|
|
472
|
+
recursive (bool): Whether to descend into sub-directories.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
list[Path]: Matching files, sorted for deterministic order.
|
|
476
|
+
"""
|
|
477
|
+
pattern = "**/*" if recursive else "*"
|
|
478
|
+
return sorted(
|
|
479
|
+
match for match in root.glob(pattern)
|
|
480
|
+
if match.is_file() and match.suffix.lower() in IMAGE_EXTENSIONS
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _expand_glob(pattern: str) -> list[Path]:
|
|
485
|
+
"""
|
|
486
|
+
Expand a glob pattern into its image file matches, sorted by path.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
pattern (str): Glob pattern (``**`` is supported).
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
list[Path]: Matching files, sorted for deterministic order.
|
|
493
|
+
"""
|
|
494
|
+
return sorted(
|
|
495
|
+
match for match in (Path(found) for found in glob.glob(pattern, recursive=True))
|
|
496
|
+
if match.is_file() and match.suffix.lower() in IMAGE_EXTENSIONS
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _assign_output_path(base_dir: Path, name: Any, taken: set[Path]) -> Path:
|
|
501
|
+
"""
|
|
502
|
+
Return a collision-free output path under ``base_dir`` for ``name``.
|
|
503
|
+
|
|
504
|
+
A relative name keeps its sub-paths (``sub/a.png``), so directory
|
|
505
|
+
inputs are mirrored. When two inputs would write to the same path, the
|
|
506
|
+
later one gets a numeric suffix (``a.png``, ``a_1.png``), matching the
|
|
507
|
+
convention of ``Result.save_as_img``.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
base_dir (Path): Output root directory.
|
|
511
|
+
name (Any): Relative output name (str or Path) for this input.
|
|
512
|
+
taken (set[Path]): Paths already assigned; updated in place.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Path: The output path for this input.
|
|
516
|
+
"""
|
|
517
|
+
candidate = base_dir.joinpath(*Path(name).parts)
|
|
518
|
+
if candidate not in taken:
|
|
519
|
+
taken.add(candidate)
|
|
520
|
+
return candidate
|
|
521
|
+
stem, suffix = candidate.stem, candidate.suffix
|
|
522
|
+
parent = candidate.parent
|
|
523
|
+
counter = 1
|
|
524
|
+
while True:
|
|
525
|
+
alternative = parent / f"{stem}_{counter}{suffix}"
|
|
526
|
+
if alternative not in taken:
|
|
527
|
+
taken.add(alternative)
|
|
528
|
+
return alternative
|
|
529
|
+
counter += 1
|