ImagePRO-Python 1.3.1__tar.gz → 1.4.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.3.1/src/ImagePRO_Python.egg-info → imagepro_python-1.4.0}/PKG-INFO +24 -1
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/pyproject.toml +1 -1
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/readme.md +23 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/__init__.py +5 -2
- imagepro_python-1.4.0/src/ImagePRO/pipeline/__init__.py +9 -0
- imagepro_python-1.4.0/src/ImagePRO/pipeline/pipeline.py +362 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0/src/ImagePRO_Python.egg-info}/PKG-INFO +24 -1
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO_Python.egg-info/SOURCES.txt +3 -0
- imagepro_python-1.4.0/tests/test_pipeline.py +357 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/LICENSE +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/setup.cfg +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/__init__.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/body_analysis/__init__.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/body_analysis/hand_tracking.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/face_analysis/__init__.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/face_analysis/face_comparison.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/face_analysis/face_detection.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/object_analysis/__init__.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/object_analysis/object_detection.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/__init__.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/blur.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/contrast.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/crop.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/dataset_generator.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/grayscale.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/histogram.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/resize.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/rotate.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/sharpen.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/utils/__init__.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/utils/image.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/utils/result.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO_Python.egg-info/dependency_links.txt +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO_Python.egg-info/requires.txt +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO_Python.egg-info/top_level.txt +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_blur.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_body_pose_estimation.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_contrast.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_crop.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_dataset_generator.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_eye_status_analysis.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_face_comparison.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_face_detection.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_face_mesh_analysis.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_grayscale.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_hand_tracking.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_head_pose_estimation.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_histogram.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_image.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_integration.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_object_detection.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_resize.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_result.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.0}/tests/test_rotate.py +0 -0
- {imagepro_python-1.3.1 → imagepro_python-1.4.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.4.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
|
|
@@ -76,6 +76,11 @@ Whether you're working on computer vision pipelines, preprocessing images for AI
|
|
|
76
76
|
- **YOLO Integration**: Multiple accuracy levels (nano to extra-large)
|
|
77
77
|
- **Flexible Models**: Pre-trained or custom model support
|
|
78
78
|
|
|
79
|
+
### **Pipelines**
|
|
80
|
+
- **Reusable Operation Chains**: Define a sequence of functions once, then run it on any input (Image, NumPy array, or file path)
|
|
81
|
+
- **Flexible Steps**: Any ImagePRO function or custom callable; per-step keyword arguments via `Step`
|
|
82
|
+
- **Pipeline Metadata**: Every result lists the executed steps and each step's parameters
|
|
83
|
+
|
|
79
84
|
## Installation
|
|
80
85
|
|
|
81
86
|
### From PyPI
|
|
@@ -154,6 +159,22 @@ object_detection_result = detect_objects(
|
|
|
154
159
|
)
|
|
155
160
|
print(f"Detected {len(object_detection_result.data)} objects")
|
|
156
161
|
object_detection_result.save_as_img("detections.jpg")
|
|
162
|
+
|
|
163
|
+
# Chain operations into a reusable pipeline
|
|
164
|
+
from ImagePRO.pipeline import Pipeline, Step
|
|
165
|
+
from ImagePRO.pre_processing.grayscale import convert_to_grayscale
|
|
166
|
+
from ImagePRO.pre_processing.resize import resize_image
|
|
167
|
+
|
|
168
|
+
pipeline = Pipeline([
|
|
169
|
+
Step(apply_gaussian_blur, kernel_size=(7, 7)),
|
|
170
|
+
convert_to_grayscale,
|
|
171
|
+
Step(resize_image, new_size=(800, 600)),
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
# Run it on any input: an Image, a NumPy array, or a file path
|
|
175
|
+
pipeline_result = pipeline.run("person_and_objects.jpg")
|
|
176
|
+
pipeline_result.save_as_img("processed_output.jpg")
|
|
177
|
+
print(pipeline_result.meta["steps"]) # Executed step names
|
|
157
178
|
```
|
|
158
179
|
|
|
159
180
|
> **Note**: These are basic examples. Each module contains many more functions with extensive customization options. Explore the module-specific README files for detailed documentation.
|
|
@@ -167,6 +188,7 @@ Each module includes comprehensive documentation with detailed examples:
|
|
|
167
188
|
- [Face Analysis](src/ImagePRO/human_analysis/face_analysis/README.md): Face mesh, pose estimation, eye status, comparison
|
|
168
189
|
- [Body Analysis](src/ImagePRO/human_analysis/body_analysis/README.md): Body pose and hand tracking
|
|
169
190
|
- **[Object Analysis](src/ImagePRO/object_analysis/README.md)**: YOLO-based object detection
|
|
191
|
+
- **[Pipeline](src/ImagePRO/pipeline/README.md)**: Reusable chains of image processing operations
|
|
170
192
|
- **[Utils](src/ImagePRO/utils/README.md)**: Shared utilities and I/O handling
|
|
171
193
|
|
|
172
194
|
For detailed project structure and development guidelines, see [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md).
|
|
@@ -178,6 +200,7 @@ ImagePRO is built with a modular architecture designed for extensibility and mai
|
|
|
178
200
|
- **Clean Separation of Concerns**: Each module handles a specific domain
|
|
179
201
|
- **Consistent API Patterns**: All functions follow the same input/output conventions
|
|
180
202
|
- **Shared Utilities**: Common `Image` and `Result` classes for unified I/O
|
|
203
|
+
- **Reusable Pipelines**: Chain any operations into a `Pipeline` and run it on any input
|
|
181
204
|
- **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
|
|
182
205
|
- **Professional Error Handling**: Comprehensive validation with clear error messages
|
|
183
206
|
- **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.4.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"
|
|
@@ -27,6 +27,11 @@ Whether you're working on computer vision pipelines, preprocessing images for AI
|
|
|
27
27
|
- **YOLO Integration**: Multiple accuracy levels (nano to extra-large)
|
|
28
28
|
- **Flexible Models**: Pre-trained or custom model support
|
|
29
29
|
|
|
30
|
+
### **Pipelines**
|
|
31
|
+
- **Reusable Operation Chains**: Define a sequence of functions once, then run it on any input (Image, NumPy array, or file path)
|
|
32
|
+
- **Flexible Steps**: Any ImagePRO function or custom callable; per-step keyword arguments via `Step`
|
|
33
|
+
- **Pipeline Metadata**: Every result lists the executed steps and each step's parameters
|
|
34
|
+
|
|
30
35
|
## Installation
|
|
31
36
|
|
|
32
37
|
### From PyPI
|
|
@@ -105,6 +110,22 @@ object_detection_result = detect_objects(
|
|
|
105
110
|
)
|
|
106
111
|
print(f"Detected {len(object_detection_result.data)} objects")
|
|
107
112
|
object_detection_result.save_as_img("detections.jpg")
|
|
113
|
+
|
|
114
|
+
# Chain operations into a reusable pipeline
|
|
115
|
+
from ImagePRO.pipeline import Pipeline, Step
|
|
116
|
+
from ImagePRO.pre_processing.grayscale import convert_to_grayscale
|
|
117
|
+
from ImagePRO.pre_processing.resize import resize_image
|
|
118
|
+
|
|
119
|
+
pipeline = Pipeline([
|
|
120
|
+
Step(apply_gaussian_blur, kernel_size=(7, 7)),
|
|
121
|
+
convert_to_grayscale,
|
|
122
|
+
Step(resize_image, new_size=(800, 600)),
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
# Run it on any input: an Image, a NumPy array, or a file path
|
|
126
|
+
pipeline_result = pipeline.run("person_and_objects.jpg")
|
|
127
|
+
pipeline_result.save_as_img("processed_output.jpg")
|
|
128
|
+
print(pipeline_result.meta["steps"]) # Executed step names
|
|
108
129
|
```
|
|
109
130
|
|
|
110
131
|
> **Note**: These are basic examples. Each module contains many more functions with extensive customization options. Explore the module-specific README files for detailed documentation.
|
|
@@ -118,6 +139,7 @@ Each module includes comprehensive documentation with detailed examples:
|
|
|
118
139
|
- [Face Analysis](src/ImagePRO/human_analysis/face_analysis/README.md): Face mesh, pose estimation, eye status, comparison
|
|
119
140
|
- [Body Analysis](src/ImagePRO/human_analysis/body_analysis/README.md): Body pose and hand tracking
|
|
120
141
|
- **[Object Analysis](src/ImagePRO/object_analysis/README.md)**: YOLO-based object detection
|
|
142
|
+
- **[Pipeline](src/ImagePRO/pipeline/README.md)**: Reusable chains of image processing operations
|
|
121
143
|
- **[Utils](src/ImagePRO/utils/README.md)**: Shared utilities and I/O handling
|
|
122
144
|
|
|
123
145
|
For detailed project structure and development guidelines, see [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md).
|
|
@@ -129,6 +151,7 @@ ImagePRO is built with a modular architecture designed for extensibility and mai
|
|
|
129
151
|
- **Clean Separation of Concerns**: Each module handles a specific domain
|
|
130
152
|
- **Consistent API Patterns**: All functions follow the same input/output conventions
|
|
131
153
|
- **Shared Utilities**: Common `Image` and `Result` classes for unified I/O
|
|
154
|
+
- **Reusable Pipelines**: Chain any operations into a `Pipeline` and run it on any input
|
|
132
155
|
- **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
|
|
133
156
|
- **Professional Error Handling**: Comprehensive validation with clear error messages
|
|
134
157
|
- **Type Safety**: Full type hints throughout the codebase
|
|
@@ -12,10 +12,11 @@ Features:
|
|
|
12
12
|
- Pre-processing: resize, crop, rotate, blur, sharpen, contrast enhancement
|
|
13
13
|
- Human analysis: face mesh, pose estimation, hand tracking
|
|
14
14
|
- Object detection with YOLO models
|
|
15
|
+
- Reusable pipelines for chaining operations
|
|
15
16
|
- Real-time processing capabilities
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
|
-
__version__ = "1.
|
|
19
|
+
__version__ = "1.4.0"
|
|
19
20
|
__author__ = "Parsa Safaie"
|
|
20
21
|
__email__ = "parsasafaie.2568@proton.me"
|
|
21
22
|
|
|
@@ -24,10 +25,12 @@ from . import utils
|
|
|
24
25
|
from . import pre_processing
|
|
25
26
|
from . import human_analysis
|
|
26
27
|
from . import object_analysis
|
|
28
|
+
from . import pipeline
|
|
27
29
|
|
|
28
30
|
__all__ = [
|
|
29
31
|
"utils",
|
|
30
32
|
"pre_processing",
|
|
31
33
|
"human_analysis",
|
|
32
|
-
"object_analysis"
|
|
34
|
+
"object_analysis",
|
|
35
|
+
"pipeline"
|
|
33
36
|
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Pipeline Module
|
|
2
|
+
# Provides a reusable, ordered chain of image processing operations
|
|
3
|
+
#
|
|
4
|
+
# Uses only base dependencies (numpy); all operations handed to a Pipeline
|
|
5
|
+
# may come from any module, including those with optional backends.
|
|
6
|
+
|
|
7
|
+
from .pipeline import Pipeline, Step
|
|
8
|
+
|
|
9
|
+
__all__ = ["Pipeline", "Step"]
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Iterator
|
|
6
|
+
|
|
7
|
+
# Add src directory to path for absolute imports
|
|
8
|
+
_file_path = Path(__file__).resolve()
|
|
9
|
+
_src_path = _file_path.parents[2] # Go up to src directory
|
|
10
|
+
if str(_src_path) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_src_path))
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ImagePRO.utils.image import Image
|
|
16
|
+
from ImagePRO.utils.result import Result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Step:
|
|
20
|
+
"""
|
|
21
|
+
A single operation in a Pipeline, with optional keyword arguments.
|
|
22
|
+
|
|
23
|
+
Wraps any callable that accepts an Image as its first positional argument
|
|
24
|
+
and returns a Result (or a numpy.ndarray, treated as the output image).
|
|
25
|
+
Keyword arguments given to the constructor are forwarded to the wrapped
|
|
26
|
+
function on every call, so a Step fully describes one stage of a pipeline.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> from ImagePRO.pre_processing.blur import apply_gaussian_blur
|
|
30
|
+
>>> step = Step(apply_gaussian_blur, kernel_size=(7, 7))
|
|
31
|
+
>>> result = step(image) # Same as apply_gaussian_blur(image, kernel_size=(7, 7))
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
function (Callable):
|
|
35
|
+
The wrapped function to execute.
|
|
36
|
+
kwargs (dict[str, Any]):
|
|
37
|
+
Keyword-only arguments forwarded to the function on each call.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, function: Callable[..., Result], **kwargs: Any) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Create a Step from a function and its keyword arguments.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
function (Callable):
|
|
46
|
+
Function to wrap. Must be callable and accept an Image as its
|
|
47
|
+
first positional argument.
|
|
48
|
+
**kwargs (Any):
|
|
49
|
+
Keyword-only arguments forwarded to the function on every call.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
TypeError: If function is not callable.
|
|
53
|
+
"""
|
|
54
|
+
if not callable(function):
|
|
55
|
+
raise TypeError("'function' must be a callable.")
|
|
56
|
+
self.function = function
|
|
57
|
+
self.kwargs: dict[str, Any] = dict(kwargs)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def name(self) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Human-readable name of the wrapped function, used in pipeline metadata.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: The function's __name__ when available, otherwise the name
|
|
66
|
+
of its type (e.g. 'partial' for functools.partial objects).
|
|
67
|
+
"""
|
|
68
|
+
return getattr(self.function, "__name__", type(self.function).__name__)
|
|
69
|
+
|
|
70
|
+
def __call__(self, image: Image) -> Result:
|
|
71
|
+
"""
|
|
72
|
+
Execute the wrapped function on an image.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
image (Image):
|
|
76
|
+
Input image passed to the wrapped function.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Result: Whatever the wrapped function returns.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
TypeError: If image is not an Image instance (raised by the
|
|
83
|
+
wrapped function's own validation).
|
|
84
|
+
ValueError: If wrapped function arguments are invalid (raised by
|
|
85
|
+
the wrapped function's own validation).
|
|
86
|
+
"""
|
|
87
|
+
return self.function(image, **self.kwargs)
|
|
88
|
+
|
|
89
|
+
def __repr__(self) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Return a readable representation of the step.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str: Representation including the function name and arguments.
|
|
95
|
+
"""
|
|
96
|
+
args = ", ".join(f"{key}={value!r}" for key, value in self.kwargs.items())
|
|
97
|
+
return f"Step({self.name}{', ' + args if args else ''})"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Pipeline:
|
|
101
|
+
"""
|
|
102
|
+
A reusable, ordered chain of image processing operations.
|
|
103
|
+
|
|
104
|
+
A Pipeline is defined once from a sequence of functions and can then be
|
|
105
|
+
run on any input as many times as needed. Each operation receives an
|
|
106
|
+
Image and its output image is automatically wrapped and fed to the next
|
|
107
|
+
operation; the final Result is returned with pipeline metadata attached.
|
|
108
|
+
|
|
109
|
+
Operations are the standard ImagePRO functions (which take an Image and
|
|
110
|
+
return a Result) or any custom callable following the same convention.
|
|
111
|
+
A custom function may also return a plain numpy.ndarray, which is treated
|
|
112
|
+
as the output image. Per-operation keyword arguments are configured with
|
|
113
|
+
Step, or fluently via apply().
|
|
114
|
+
|
|
115
|
+
Pipelines are immutable: apply() returns a new Pipeline and leaves the
|
|
116
|
+
original untouched, and run() never mutates the pipeline, so a single
|
|
117
|
+
Pipeline instance is safe to reuse across many inputs.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> from ImagePRO.pre_processing.blur import apply_gaussian_blur
|
|
121
|
+
>>> from ImagePRO.pre_processing.grayscale import convert_to_grayscale
|
|
122
|
+
>>> from ImagePRO.pre_processing.resize import resize_image
|
|
123
|
+
>>>
|
|
124
|
+
>>> pipeline = Pipeline([
|
|
125
|
+
... Step(apply_gaussian_blur, kernel_size=(7, 7)),
|
|
126
|
+
... convert_to_grayscale,
|
|
127
|
+
... Step(resize_image, new_size=(800, 600)),
|
|
128
|
+
... ])
|
|
129
|
+
>>> result = pipeline.run("photo.jpg") # Any Image, ndarray or path
|
|
130
|
+
>>> result.save_as_img("processed.jpg")
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, *operations: Any) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Create a Pipeline from one or more operations.
|
|
136
|
+
|
|
137
|
+
Operations can be passed as separate arguments or as a single
|
|
138
|
+
list/tuple. Passing another Pipeline as an operation nests it: the
|
|
139
|
+
inner pipeline runs as a single step.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
*operations (Callable | Step | list | tuple):
|
|
143
|
+
Functions to execute in order. Each element must be callable;
|
|
144
|
+
Step instances carry their keyword arguments. A single
|
|
145
|
+
list/tuple of operations is also accepted.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
TypeError: If any operation is not callable.
|
|
149
|
+
"""
|
|
150
|
+
if len(operations) == 1 and isinstance(operations[0], (list, tuple)):
|
|
151
|
+
operations = tuple(operations[0])
|
|
152
|
+
|
|
153
|
+
steps: list[Step] = []
|
|
154
|
+
for operation in operations:
|
|
155
|
+
if not callable(operation):
|
|
156
|
+
raise TypeError(
|
|
157
|
+
"All pipeline operations must be callable; got "
|
|
158
|
+
f"{type(operation).__name__}. Wrap keyword arguments with "
|
|
159
|
+
"Step, e.g. Step(resize_image, new_size=(800, 600))."
|
|
160
|
+
)
|
|
161
|
+
steps.append(operation if isinstance(operation, Step) else Step(operation))
|
|
162
|
+
self._operations = steps
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def operations(self) -> tuple[Step, ...]:
|
|
166
|
+
"""
|
|
167
|
+
The ordered steps of this pipeline.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
tuple[Step, ...]: Step objects in execution order.
|
|
171
|
+
"""
|
|
172
|
+
return tuple(self._operations)
|
|
173
|
+
|
|
174
|
+
def apply(self, function: Callable[..., Result], **kwargs: Any) -> Pipeline:
|
|
175
|
+
"""
|
|
176
|
+
Return a new Pipeline with one more operation appended.
|
|
177
|
+
|
|
178
|
+
The current pipeline is not modified, following ImagePRO's
|
|
179
|
+
non-destructive design principle.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
function (Callable):
|
|
183
|
+
Function to append. Must be callable and accept an Image as
|
|
184
|
+
its first positional argument.
|
|
185
|
+
**kwargs (Any):
|
|
186
|
+
Keyword-only arguments forwarded to the function on every run.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Pipeline: New pipeline containing all existing steps plus this one.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
TypeError: If function is not callable.
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
>>> pipeline = Pipeline(apply_gaussian_blur).apply(
|
|
196
|
+
... resize_image, new_size=(800, 600)
|
|
197
|
+
... )
|
|
198
|
+
"""
|
|
199
|
+
return Pipeline(*self._operations, Step(function, **kwargs))
|
|
200
|
+
|
|
201
|
+
def run(self, image: Image | np.ndarray | str | Path) -> Result:
|
|
202
|
+
"""
|
|
203
|
+
Execute all pipeline operations in order on the given input.
|
|
204
|
+
|
|
205
|
+
The input is first normalized to an Image (file paths are loaded and
|
|
206
|
+
NumPy arrays are wrapped, assuming BGR for 3-channel and GRAY for 2D
|
|
207
|
+
arrays). Each operation then receives an Image built from the previous
|
|
208
|
+
operation's output; 2D intermediates are tagged as GRAY and
|
|
209
|
+
multi-channel ones keep the incoming colorspace.
|
|
210
|
+
|
|
211
|
+
The returned Result carries the final operation's image and data. Its
|
|
212
|
+
metadata contains the pipeline information (operation, steps,
|
|
213
|
+
step_meta, source) merged over the final operation's own metadata.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
image (Image | np.ndarray | str | Path):
|
|
217
|
+
Input to process. Can be an Image instance, a NumPy array
|
|
218
|
+
(BGR for 3-channel, GRAY for 2D), or a path to an image file.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Result: Result object of the final operation.
|
|
222
|
+
- image (np.ndarray | list[np.ndarray] | None): Final output image(s)
|
|
223
|
+
- data (Any): Structured data of the final operation
|
|
224
|
+
- meta (dict): Final operation metadata plus pipeline info:
|
|
225
|
+
'operation' ("pipeline"), 'steps' (list of step names),
|
|
226
|
+
'step_meta' (per-step metadata list), 'source' (input Image)
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
TypeError: If image is not an Image, ndarray, or path.
|
|
230
|
+
TypeError: If an operation returns an unsupported type.
|
|
231
|
+
ValueError: If the pipeline has no operations.
|
|
232
|
+
ValueError: If a non-final operation returns no image, so the
|
|
233
|
+
chain cannot continue.
|
|
234
|
+
TypeError/ValueError: As raised by the operations themselves for
|
|
235
|
+
invalid inputs or arguments.
|
|
236
|
+
"""
|
|
237
|
+
# Normalize input to an Image (cheap validation before any step runs)
|
|
238
|
+
if isinstance(image, Image):
|
|
239
|
+
current = image
|
|
240
|
+
elif isinstance(image, np.ndarray):
|
|
241
|
+
colorspace = "GRAY" if image.ndim == 2 else "BGR"
|
|
242
|
+
current = Image.from_array(image, colorspace=colorspace)
|
|
243
|
+
elif isinstance(image, (str, Path)):
|
|
244
|
+
current = Image.from_path(image)
|
|
245
|
+
else:
|
|
246
|
+
raise TypeError(
|
|
247
|
+
"'image' must be an Image instance, a numpy.ndarray, or a "
|
|
248
|
+
"file path (str or pathlib.Path)."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if not self._operations:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
"Cannot run an empty pipeline. Add operations at construction "
|
|
254
|
+
"or with apply()."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
source = current
|
|
258
|
+
step_names: list[str] = []
|
|
259
|
+
step_metas: list[dict[str, Any]] = []
|
|
260
|
+
final_image: Any = None
|
|
261
|
+
final_data: Any = None
|
|
262
|
+
final_meta: dict[str, Any] = {}
|
|
263
|
+
|
|
264
|
+
for index, step in enumerate(self._operations):
|
|
265
|
+
is_last = index == len(self._operations) - 1
|
|
266
|
+
output = step(current)
|
|
267
|
+
|
|
268
|
+
# Normalize the step output (Result is the convention; a plain
|
|
269
|
+
# ndarray from custom functions is accepted as the output image)
|
|
270
|
+
if isinstance(output, Result):
|
|
271
|
+
final_image = output.image
|
|
272
|
+
final_data = output.data
|
|
273
|
+
final_meta = dict(output.meta)
|
|
274
|
+
elif isinstance(output, np.ndarray):
|
|
275
|
+
final_image = output
|
|
276
|
+
final_data = None
|
|
277
|
+
final_meta = {}
|
|
278
|
+
else:
|
|
279
|
+
raise TypeError(
|
|
280
|
+
f"Step {index} ('{step.name}') returned "
|
|
281
|
+
f"{type(output).__name__}; pipeline operations must "
|
|
282
|
+
"return a Result object or a numpy.ndarray."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
step_names.append(step.name)
|
|
286
|
+
step_metas.append(final_meta)
|
|
287
|
+
|
|
288
|
+
if is_last:
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
# Prepare the input of the next operation
|
|
292
|
+
if final_image is None:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"Step {index} ('{step.name}') returned no image, so the "
|
|
295
|
+
"pipeline cannot continue. Only the final step of a "
|
|
296
|
+
"pipeline may return a result without an image."
|
|
297
|
+
)
|
|
298
|
+
if not isinstance(final_image, np.ndarray):
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"Step {index} ('{step.name}') did not return a single "
|
|
301
|
+
"image array, so the pipeline cannot continue. Only the "
|
|
302
|
+
"final step of a pipeline may return multi-image results."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
colorspace = "GRAY" if final_image.ndim == 2 else current.colorspace
|
|
306
|
+
current = Image.from_array(final_image, colorspace=colorspace)
|
|
307
|
+
|
|
308
|
+
meta = dict(final_meta)
|
|
309
|
+
meta.update({
|
|
310
|
+
"source": source,
|
|
311
|
+
"operation": "pipeline",
|
|
312
|
+
"steps": step_names,
|
|
313
|
+
"step_meta": step_metas,
|
|
314
|
+
})
|
|
315
|
+
return Result(image=final_image, data=final_data, meta=meta)
|
|
316
|
+
|
|
317
|
+
def __call__(self, image: Image | np.ndarray | str | Path) -> Result:
|
|
318
|
+
"""
|
|
319
|
+
Run the pipeline on an input; shorthand for run().
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
image (Image | np.ndarray | str | Path):
|
|
323
|
+
Input to process, as accepted by run().
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Result: Result object of the final operation.
|
|
327
|
+
|
|
328
|
+
Raises:
|
|
329
|
+
TypeError: If image is not an Image, ndarray, or path.
|
|
330
|
+
ValueError: If the pipeline has no operations or a non-final
|
|
331
|
+
operation returns no image.
|
|
332
|
+
"""
|
|
333
|
+
return self.run(image)
|
|
334
|
+
|
|
335
|
+
def __len__(self) -> int:
|
|
336
|
+
"""
|
|
337
|
+
Return the number of operations in the pipeline.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
int: Number of steps.
|
|
341
|
+
"""
|
|
342
|
+
return len(self._operations)
|
|
343
|
+
|
|
344
|
+
def __iter__(self) -> Iterator[Step]:
|
|
345
|
+
"""
|
|
346
|
+
Iterate over the pipeline's steps in execution order.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Iterator[Step]: Iterator of Step objects.
|
|
350
|
+
"""
|
|
351
|
+
return iter(self._operations)
|
|
352
|
+
|
|
353
|
+
def __repr__(self) -> str:
|
|
354
|
+
"""
|
|
355
|
+
Return a readable representation of the pipeline.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
str: Representation listing the step names in order.
|
|
359
|
+
"""
|
|
360
|
+
if not self._operations:
|
|
361
|
+
return "Pipeline(<empty>)"
|
|
362
|
+
return f"Pipeline({' -> '.join(step.name for step in self._operations)})"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ImagePRO-Python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -76,6 +76,11 @@ Whether you're working on computer vision pipelines, preprocessing images for AI
|
|
|
76
76
|
- **YOLO Integration**: Multiple accuracy levels (nano to extra-large)
|
|
77
77
|
- **Flexible Models**: Pre-trained or custom model support
|
|
78
78
|
|
|
79
|
+
### **Pipelines**
|
|
80
|
+
- **Reusable Operation Chains**: Define a sequence of functions once, then run it on any input (Image, NumPy array, or file path)
|
|
81
|
+
- **Flexible Steps**: Any ImagePRO function or custom callable; per-step keyword arguments via `Step`
|
|
82
|
+
- **Pipeline Metadata**: Every result lists the executed steps and each step's parameters
|
|
83
|
+
|
|
79
84
|
## Installation
|
|
80
85
|
|
|
81
86
|
### From PyPI
|
|
@@ -154,6 +159,22 @@ object_detection_result = detect_objects(
|
|
|
154
159
|
)
|
|
155
160
|
print(f"Detected {len(object_detection_result.data)} objects")
|
|
156
161
|
object_detection_result.save_as_img("detections.jpg")
|
|
162
|
+
|
|
163
|
+
# Chain operations into a reusable pipeline
|
|
164
|
+
from ImagePRO.pipeline import Pipeline, Step
|
|
165
|
+
from ImagePRO.pre_processing.grayscale import convert_to_grayscale
|
|
166
|
+
from ImagePRO.pre_processing.resize import resize_image
|
|
167
|
+
|
|
168
|
+
pipeline = Pipeline([
|
|
169
|
+
Step(apply_gaussian_blur, kernel_size=(7, 7)),
|
|
170
|
+
convert_to_grayscale,
|
|
171
|
+
Step(resize_image, new_size=(800, 600)),
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
# Run it on any input: an Image, a NumPy array, or a file path
|
|
175
|
+
pipeline_result = pipeline.run("person_and_objects.jpg")
|
|
176
|
+
pipeline_result.save_as_img("processed_output.jpg")
|
|
177
|
+
print(pipeline_result.meta["steps"]) # Executed step names
|
|
157
178
|
```
|
|
158
179
|
|
|
159
180
|
> **Note**: These are basic examples. Each module contains many more functions with extensive customization options. Explore the module-specific README files for detailed documentation.
|
|
@@ -167,6 +188,7 @@ Each module includes comprehensive documentation with detailed examples:
|
|
|
167
188
|
- [Face Analysis](src/ImagePRO/human_analysis/face_analysis/README.md): Face mesh, pose estimation, eye status, comparison
|
|
168
189
|
- [Body Analysis](src/ImagePRO/human_analysis/body_analysis/README.md): Body pose and hand tracking
|
|
169
190
|
- **[Object Analysis](src/ImagePRO/object_analysis/README.md)**: YOLO-based object detection
|
|
191
|
+
- **[Pipeline](src/ImagePRO/pipeline/README.md)**: Reusable chains of image processing operations
|
|
170
192
|
- **[Utils](src/ImagePRO/utils/README.md)**: Shared utilities and I/O handling
|
|
171
193
|
|
|
172
194
|
For detailed project structure and development guidelines, see [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md).
|
|
@@ -178,6 +200,7 @@ ImagePRO is built with a modular architecture designed for extensibility and mai
|
|
|
178
200
|
- **Clean Separation of Concerns**: Each module handles a specific domain
|
|
179
201
|
- **Consistent API Patterns**: All functions follow the same input/output conventions
|
|
180
202
|
- **Shared Utilities**: Common `Image` and `Result` classes for unified I/O
|
|
203
|
+
- **Reusable Pipelines**: Chain any operations into a `Pipeline` and run it on any input
|
|
181
204
|
- **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
|
|
182
205
|
- **Professional Error Handling**: Comprehensive validation with clear error messages
|
|
183
206
|
- **Type Safety**: Full type hints throughout the codebase
|
|
@@ -14,6 +14,8 @@ src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py
|
|
|
14
14
|
src/ImagePRO/human_analysis/face_analysis/head_pose_estimation.py
|
|
15
15
|
src/ImagePRO/object_analysis/__init__.py
|
|
16
16
|
src/ImagePRO/object_analysis/object_detection.py
|
|
17
|
+
src/ImagePRO/pipeline/__init__.py
|
|
18
|
+
src/ImagePRO/pipeline/pipeline.py
|
|
17
19
|
src/ImagePRO/pre_processing/__init__.py
|
|
18
20
|
src/ImagePRO/pre_processing/blur.py
|
|
19
21
|
src/ImagePRO/pre_processing/contrast.py
|
|
@@ -48,6 +50,7 @@ tests/test_histogram.py
|
|
|
48
50
|
tests/test_image.py
|
|
49
51
|
tests/test_integration.py
|
|
50
52
|
tests/test_object_detection.py
|
|
53
|
+
tests/test_pipeline.py
|
|
51
54
|
tests/test_resize.py
|
|
52
55
|
tests/test_result.py
|
|
53
56
|
tests/test_rotate.py
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Unit tests for ImagePRO.pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import partial
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from ImagePRO.pipeline import Pipeline, Step
|
|
11
|
+
from ImagePRO.pre_processing.blur import apply_gaussian_blur
|
|
12
|
+
from ImagePRO.pre_processing.grayscale import convert_to_grayscale
|
|
13
|
+
from ImagePRO.pre_processing.resize import resize_image
|
|
14
|
+
from ImagePRO.utils.image import Image
|
|
15
|
+
from ImagePRO.utils.result import Result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def multiply_function():
|
|
20
|
+
"""Custom operation following the Image -> Result convention."""
|
|
21
|
+
|
|
22
|
+
def function(image, *, scale=1):
|
|
23
|
+
output = image._data if scale == 1 else np.clip(image._data * scale, 0, 255).astype(np.uint8)
|
|
24
|
+
return Result(image=output, meta={"operation": "multiply", "scale": scale})
|
|
25
|
+
|
|
26
|
+
return function
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def colorspace_recorder():
|
|
31
|
+
"""Custom operation recording the colorspace of every Image received."""
|
|
32
|
+
seen = []
|
|
33
|
+
|
|
34
|
+
def function(image):
|
|
35
|
+
seen.append(image.colorspace)
|
|
36
|
+
return Result(image=image._data)
|
|
37
|
+
|
|
38
|
+
function.seen = seen
|
|
39
|
+
return function
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestStep:
|
|
43
|
+
def test_forwards_kwargs_to_function(self, sample_bgr_image):
|
|
44
|
+
step = Step(apply_gaussian_blur, kernel_size=(9, 9))
|
|
45
|
+
result = step(sample_bgr_image)
|
|
46
|
+
assert result.meta["kernel_size"] == (9, 9)
|
|
47
|
+
|
|
48
|
+
def test_works_without_kwargs(self, sample_bgr_image):
|
|
49
|
+
step = Step(apply_gaussian_blur)
|
|
50
|
+
result = step(sample_bgr_image)
|
|
51
|
+
assert result.meta["kernel_size"] == (5, 5)
|
|
52
|
+
|
|
53
|
+
def test_name_property(self):
|
|
54
|
+
assert Step(apply_gaussian_blur).name == "apply_gaussian_blur"
|
|
55
|
+
|
|
56
|
+
def test_name_falls_back_to_type_name(self):
|
|
57
|
+
step = Step(partial(apply_gaussian_blur, kernel_size=(3, 3)))
|
|
58
|
+
assert step.name == "partial"
|
|
59
|
+
|
|
60
|
+
def test_non_callable_raises(self):
|
|
61
|
+
with pytest.raises(TypeError):
|
|
62
|
+
Step("not callable")
|
|
63
|
+
|
|
64
|
+
def test_repr_shows_name_and_kwargs(self):
|
|
65
|
+
step = Step(apply_gaussian_blur, kernel_size=(7, 7))
|
|
66
|
+
assert "apply_gaussian_blur" in repr(step)
|
|
67
|
+
assert "kernel_size=(7, 7)" in repr(step)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestConstruction:
|
|
71
|
+
def test_from_list(self):
|
|
72
|
+
pipeline = Pipeline([apply_gaussian_blur, convert_to_grayscale])
|
|
73
|
+
assert len(pipeline) == 2
|
|
74
|
+
|
|
75
|
+
def test_from_tuple(self):
|
|
76
|
+
pipeline = Pipeline((apply_gaussian_blur, convert_to_grayscale))
|
|
77
|
+
assert len(pipeline) == 2
|
|
78
|
+
|
|
79
|
+
def test_from_varargs(self):
|
|
80
|
+
pipeline = Pipeline(apply_gaussian_blur, convert_to_grayscale)
|
|
81
|
+
assert len(pipeline) == 2
|
|
82
|
+
|
|
83
|
+
def test_from_single_callable(self):
|
|
84
|
+
pipeline = Pipeline(apply_gaussian_blur)
|
|
85
|
+
assert len(pipeline) == 1
|
|
86
|
+
|
|
87
|
+
def test_from_single_step(self):
|
|
88
|
+
pipeline = Pipeline(Step(apply_gaussian_blur, kernel_size=(7, 7)))
|
|
89
|
+
assert len(pipeline) == 1
|
|
90
|
+
assert pipeline.operations[0].kwargs == {"kernel_size": (7, 7)}
|
|
91
|
+
|
|
92
|
+
def test_mixed_callables_and_steps(self):
|
|
93
|
+
pipeline = Pipeline(
|
|
94
|
+
Step(apply_gaussian_blur, kernel_size=(3, 3)),
|
|
95
|
+
convert_to_grayscale,
|
|
96
|
+
)
|
|
97
|
+
assert len(pipeline) == 2
|
|
98
|
+
assert isinstance(pipeline.operations[0], Step)
|
|
99
|
+
assert isinstance(pipeline.operations[1], Step)
|
|
100
|
+
|
|
101
|
+
def test_empty_pipeline_is_allowed(self):
|
|
102
|
+
assert len(Pipeline()) == 0
|
|
103
|
+
|
|
104
|
+
def test_plain_callables_are_wrapped_in_steps(self):
|
|
105
|
+
pipeline = Pipeline(apply_gaussian_blur)
|
|
106
|
+
assert isinstance(pipeline.operations[0], Step)
|
|
107
|
+
assert pipeline.operations[0].function is apply_gaussian_blur
|
|
108
|
+
|
|
109
|
+
def test_operations_property_returns_tuple(self):
|
|
110
|
+
pipeline = Pipeline(apply_gaussian_blur, convert_to_grayscale)
|
|
111
|
+
assert isinstance(pipeline.operations, tuple)
|
|
112
|
+
assert [step.name for step in pipeline.operations] == [
|
|
113
|
+
"apply_gaussian_blur",
|
|
114
|
+
"convert_to_grayscale",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize("bad", [42, "blur", None, 3.14])
|
|
118
|
+
def test_non_callable_operation_raises(self, bad):
|
|
119
|
+
with pytest.raises(TypeError):
|
|
120
|
+
Pipeline(bad)
|
|
121
|
+
|
|
122
|
+
def test_non_callable_inside_list_raises(self):
|
|
123
|
+
with pytest.raises(TypeError):
|
|
124
|
+
Pipeline([apply_gaussian_blur, 42])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestApply:
|
|
128
|
+
def test_returns_new_pipeline(self):
|
|
129
|
+
original = Pipeline(apply_gaussian_blur)
|
|
130
|
+
extended = original.apply(convert_to_grayscale)
|
|
131
|
+
assert extended is not original
|
|
132
|
+
assert len(original) == 1
|
|
133
|
+
assert len(extended) == 2
|
|
134
|
+
|
|
135
|
+
def test_is_chainable(self):
|
|
136
|
+
pipeline = (
|
|
137
|
+
Pipeline()
|
|
138
|
+
.apply(apply_gaussian_blur, kernel_size=(3, 3))
|
|
139
|
+
.apply(convert_to_grayscale)
|
|
140
|
+
.apply(resize_image, new_size=(16, 12))
|
|
141
|
+
)
|
|
142
|
+
assert len(pipeline) == 3
|
|
143
|
+
assert [step.name for step in pipeline] == [
|
|
144
|
+
"apply_gaussian_blur",
|
|
145
|
+
"convert_to_grayscale",
|
|
146
|
+
"resize_image",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
def test_forwards_kwargs(self, sample_bgr_image):
|
|
150
|
+
pipeline = Pipeline().apply(apply_gaussian_blur, kernel_size=(9, 9))
|
|
151
|
+
result = pipeline.run(sample_bgr_image)
|
|
152
|
+
assert result.meta["step_meta"][0]["kernel_size"] == (9, 9)
|
|
153
|
+
|
|
154
|
+
def test_non_callable_raises(self):
|
|
155
|
+
with pytest.raises(TypeError):
|
|
156
|
+
Pipeline().apply(42)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestRun:
|
|
160
|
+
def test_single_step_returns_result(self, sample_bgr_image):
|
|
161
|
+
result = Pipeline(apply_gaussian_blur).run(sample_bgr_image)
|
|
162
|
+
assert isinstance(result, Result)
|
|
163
|
+
assert result.image.shape == sample_bgr_image.shape
|
|
164
|
+
assert result.image.dtype == np.uint8
|
|
165
|
+
|
|
166
|
+
def test_multi_step_matches_manual_application(self, sample_bgr_image):
|
|
167
|
+
pipeline = Pipeline([
|
|
168
|
+
apply_gaussian_blur,
|
|
169
|
+
convert_to_grayscale,
|
|
170
|
+
Step(resize_image, new_size=(16, 12)),
|
|
171
|
+
])
|
|
172
|
+
result = pipeline.run(sample_bgr_image)
|
|
173
|
+
|
|
174
|
+
blurred = apply_gaussian_blur(image=sample_bgr_image).image
|
|
175
|
+
gray = convert_to_grayscale(
|
|
176
|
+
image=Image.from_array(blurred, colorspace="BGR")
|
|
177
|
+
).image
|
|
178
|
+
resized = resize_image(
|
|
179
|
+
image=Image.from_array(gray, colorspace="GRAY"), new_size=(16, 12)
|
|
180
|
+
).image
|
|
181
|
+
assert np.array_equal(result.image, resized)
|
|
182
|
+
|
|
183
|
+
def test_meta_contents(self, sample_bgr_image):
|
|
184
|
+
pipeline = Pipeline([
|
|
185
|
+
Step(apply_gaussian_blur, kernel_size=(7, 7)),
|
|
186
|
+
convert_to_grayscale,
|
|
187
|
+
])
|
|
188
|
+
result = pipeline.run(sample_bgr_image)
|
|
189
|
+
assert result.meta["operation"] == "pipeline"
|
|
190
|
+
assert result.meta["steps"] == ["apply_gaussian_blur", "convert_to_grayscale"]
|
|
191
|
+
assert len(result.meta["step_meta"]) == 2
|
|
192
|
+
assert result.meta["step_meta"][0]["kernel_size"] == (7, 7)
|
|
193
|
+
assert result.meta["step_meta"][1]["operation"] == "convert_to_grayscale"
|
|
194
|
+
assert result.meta["source"] is sample_bgr_image
|
|
195
|
+
|
|
196
|
+
def test_final_step_meta_is_merged(self, sample_bgr_image):
|
|
197
|
+
result = Pipeline(
|
|
198
|
+
Step(apply_gaussian_blur, kernel_size=(7, 7))
|
|
199
|
+
).run(sample_bgr_image)
|
|
200
|
+
# Pipeline keys win over the final step's own metadata
|
|
201
|
+
assert result.meta["operation"] == "pipeline"
|
|
202
|
+
assert result.meta["source"] is sample_bgr_image
|
|
203
|
+
# The final step's own keys survive the merge
|
|
204
|
+
assert result.meta["kernel_size"] == (7, 7)
|
|
205
|
+
|
|
206
|
+
def test_input_as_ndarray(self, sample_bgr_array):
|
|
207
|
+
result = Pipeline(apply_gaussian_blur).run(sample_bgr_array)
|
|
208
|
+
assert result.image.shape == sample_bgr_array.shape
|
|
209
|
+
|
|
210
|
+
def test_input_as_2d_ndarray_is_treated_as_gray(self, sample_gray_image):
|
|
211
|
+
result = Pipeline(apply_gaussian_blur).run(sample_gray_image._data)
|
|
212
|
+
assert result.meta["source"].colorspace == "GRAY"
|
|
213
|
+
|
|
214
|
+
def test_input_as_path(self, image_file):
|
|
215
|
+
result = Pipeline(convert_to_grayscale).run(image_file)
|
|
216
|
+
assert result.image.ndim == 2
|
|
217
|
+
|
|
218
|
+
def test_input_as_str_path(self, image_file):
|
|
219
|
+
result = Pipeline(convert_to_grayscale).run(str(image_file))
|
|
220
|
+
assert result.image.ndim == 2
|
|
221
|
+
|
|
222
|
+
def test_invalid_input_type_raises(self, sample_bgr_image):
|
|
223
|
+
pipeline = Pipeline(apply_gaussian_blur)
|
|
224
|
+
with pytest.raises(TypeError):
|
|
225
|
+
pipeline.run(123)
|
|
226
|
+
with pytest.raises(TypeError):
|
|
227
|
+
pipeline.run(None)
|
|
228
|
+
with pytest.raises(TypeError):
|
|
229
|
+
pipeline.run([sample_bgr_image])
|
|
230
|
+
|
|
231
|
+
def test_empty_pipeline_raises(self, sample_bgr_image):
|
|
232
|
+
with pytest.raises(ValueError):
|
|
233
|
+
Pipeline().run(sample_bgr_image)
|
|
234
|
+
|
|
235
|
+
def test_pipeline_is_reusable_across_inputs(self, multiply_function):
|
|
236
|
+
pipeline = Pipeline(multiply_function)
|
|
237
|
+
first = pipeline.run(Image.from_array(np.full((8, 8, 3), 10, np.uint8)))
|
|
238
|
+
second = pipeline.run(Image.from_array(np.full((12, 6, 3), 200, np.uint8)))
|
|
239
|
+
assert len(pipeline) == 1
|
|
240
|
+
assert first.image.shape == (8, 8, 3)
|
|
241
|
+
assert second.image.shape == (12, 6, 3)
|
|
242
|
+
|
|
243
|
+
def test_reruns_do_not_interfere(self, sample_bgr_image):
|
|
244
|
+
pipeline = Pipeline(apply_gaussian_blur)
|
|
245
|
+
first = pipeline.run(sample_bgr_image)
|
|
246
|
+
second = pipeline.run(sample_bgr_image)
|
|
247
|
+
assert np.array_equal(first.image, second.image)
|
|
248
|
+
assert first.image is not second.image
|
|
249
|
+
|
|
250
|
+
def test_custom_operation_returning_ndarray(self, sample_bgr_image):
|
|
251
|
+
def double(image):
|
|
252
|
+
return np.clip(image._data * 2, 0, 255).astype(np.uint8)
|
|
253
|
+
|
|
254
|
+
result = Pipeline(double).run(sample_bgr_image)
|
|
255
|
+
assert isinstance(result, Result)
|
|
256
|
+
assert result.meta["steps"] == ["double"]
|
|
257
|
+
expected = np.clip(sample_bgr_image._data * 2, 0, 255).astype(np.uint8)
|
|
258
|
+
assert np.array_equal(result.image, expected)
|
|
259
|
+
|
|
260
|
+
def test_unsupported_operation_return_raises(self, sample_bgr_image):
|
|
261
|
+
def bad_operation(image):
|
|
262
|
+
return "not an image or result"
|
|
263
|
+
|
|
264
|
+
with pytest.raises(TypeError):
|
|
265
|
+
Pipeline(bad_operation).run(sample_bgr_image)
|
|
266
|
+
|
|
267
|
+
def test_non_final_step_without_image_raises(self, sample_bgr_image):
|
|
268
|
+
def data_only(image):
|
|
269
|
+
return Result(image=None, data=[1, 2, 3])
|
|
270
|
+
|
|
271
|
+
pipeline = Pipeline(data_only, apply_gaussian_blur)
|
|
272
|
+
with pytest.raises(ValueError):
|
|
273
|
+
pipeline.run(sample_bgr_image)
|
|
274
|
+
|
|
275
|
+
def test_final_step_may_omit_image(self, sample_bgr_image):
|
|
276
|
+
def data_only(image):
|
|
277
|
+
return Result(image=None, data=[1, 2, 3])
|
|
278
|
+
|
|
279
|
+
result = Pipeline(data_only).run(sample_bgr_image)
|
|
280
|
+
assert result.image is None
|
|
281
|
+
assert result.data == [1, 2, 3]
|
|
282
|
+
assert result.meta["steps"] == ["data_only"]
|
|
283
|
+
|
|
284
|
+
def test_final_step_may_return_image_list(self, sample_bgr_image):
|
|
285
|
+
def two_images(image):
|
|
286
|
+
return Result(image=[image._data, image._data.copy()])
|
|
287
|
+
|
|
288
|
+
result = Pipeline(two_images).run(sample_bgr_image)
|
|
289
|
+
assert isinstance(result.image, list)
|
|
290
|
+
assert len(result.image) == 2
|
|
291
|
+
|
|
292
|
+
def test_non_final_step_with_image_list_raises(self, sample_bgr_image):
|
|
293
|
+
def two_images(image):
|
|
294
|
+
return Result(image=[image._data, image._data.copy()])
|
|
295
|
+
|
|
296
|
+
pipeline = Pipeline(two_images, apply_gaussian_blur)
|
|
297
|
+
with pytest.raises(ValueError):
|
|
298
|
+
pipeline.run(sample_bgr_image)
|
|
299
|
+
|
|
300
|
+
def test_colorspace_propagates_through_chain(
|
|
301
|
+
self, sample_bgr_image, colorspace_recorder
|
|
302
|
+
):
|
|
303
|
+
Pipeline(convert_to_grayscale, colorspace_recorder).run(sample_bgr_image)
|
|
304
|
+
assert colorspace_recorder.seen == ["GRAY"]
|
|
305
|
+
|
|
306
|
+
def test_bgr_colorspace_propagates(self, sample_bgr_image, colorspace_recorder):
|
|
307
|
+
Pipeline(apply_gaussian_blur, colorspace_recorder).run(sample_bgr_image)
|
|
308
|
+
assert colorspace_recorder.seen == ["BGR"]
|
|
309
|
+
|
|
310
|
+
def test_operation_validation_errors_propagate(self, sample_gray_image):
|
|
311
|
+
# convert_to_grayscale refuses already-gray images; the pipeline
|
|
312
|
+
# must surface that error unchanged.
|
|
313
|
+
with pytest.raises(ValueError):
|
|
314
|
+
Pipeline(convert_to_grayscale).run(sample_gray_image)
|
|
315
|
+
|
|
316
|
+
def test_nested_pipeline_runs_as_single_step(self, sample_bgr_image):
|
|
317
|
+
inner = Pipeline(apply_gaussian_blur)
|
|
318
|
+
outer = Pipeline(inner, convert_to_grayscale)
|
|
319
|
+
result = outer.run(sample_bgr_image)
|
|
320
|
+
assert result.image.ndim == 2
|
|
321
|
+
assert result.meta["steps"][0] == "Pipeline"
|
|
322
|
+
|
|
323
|
+
def test_source_meta_is_original_input(self, image_file):
|
|
324
|
+
image = Image.from_path(image_file)
|
|
325
|
+
result = Pipeline(apply_gaussian_blur).run(image)
|
|
326
|
+
assert result.meta["source"] is image
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestCall:
|
|
330
|
+
def test_call_is_shorthand_for_run(self, sample_bgr_image):
|
|
331
|
+
pipeline = Pipeline(apply_gaussian_blur)
|
|
332
|
+
by_call = pipeline(sample_bgr_image)
|
|
333
|
+
by_run = pipeline.run(sample_bgr_image)
|
|
334
|
+
assert np.array_equal(by_call.image, by_run.image)
|
|
335
|
+
assert by_call.meta["operation"] == "pipeline"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class TestDunders:
|
|
339
|
+
def test_len(self):
|
|
340
|
+
assert len(Pipeline()) == 0
|
|
341
|
+
assert len(Pipeline(apply_gaussian_blur, convert_to_grayscale)) == 2
|
|
342
|
+
|
|
343
|
+
def test_iter_yields_steps(self):
|
|
344
|
+
pipeline = Pipeline(apply_gaussian_blur, convert_to_grayscale)
|
|
345
|
+
steps = list(pipeline)
|
|
346
|
+
assert all(isinstance(step, Step) for step in steps)
|
|
347
|
+
assert [step.name for step in steps] == [
|
|
348
|
+
"apply_gaussian_blur",
|
|
349
|
+
"convert_to_grayscale",
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
def test_repr_lists_step_names(self):
|
|
353
|
+
pipeline = Pipeline(apply_gaussian_blur, convert_to_grayscale)
|
|
354
|
+
assert repr(pipeline) == "Pipeline(apply_gaussian_blur -> convert_to_grayscale)"
|
|
355
|
+
|
|
356
|
+
def test_repr_of_empty_pipeline(self):
|
|
357
|
+
assert repr(Pipeline()) == "Pipeline(<empty>)"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/object_analysis/object_detection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO/pre_processing/dataset_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imagepro_python-1.3.1 → imagepro_python-1.4.0}/src/ImagePRO_Python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|