ImagePRO-Python 1.5.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -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.5.0"
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ImagePRO-Python
3
- Version: 1.5.0
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
@@ -1,4 +1,6 @@
1
- ImagePRO/__init__.py,sha256=Rv5QiWX7e-OlR3DoLBaV5394xw6jwmarJrr2d0_RN50,1081
1
+ ImagePRO/__init__.py,sha256=4HbuhcRQYubrov1SsTGtLwOMc_MMeHrLeVWvgzNGt90,1177
2
+ ImagePRO/batch/__init__.py,sha256=WYZElEqJifbm3W_XOLM-sBNh8Q7vNDgW6rBpqWvlT_4,388
3
+ ImagePRO/batch/batch.py,sha256=721_j0wNBhmwLjTgtoknZ-5pRZ_Wc_vHmf3HqlbDKds,21357
2
4
  ImagePRO/human_analysis/__init__.py,sha256=uSU0lM1p1HM6TNGv6u0nPHWy9QY505v43lqTkhClddM,190
3
5
  ImagePRO/human_analysis/body_analysis/__init__.py,sha256=uUWG5eOxLQjZoYddmNkRlF66l6vSVyJpUFz1wDddXkk,374
4
6
  ImagePRO/human_analysis/body_analysis/body_pose_estimation.py,sha256=gLWM5N2cMUNxyRX-7_z_52MnE8J9JM_UgAPoUWPCFpY,6567
@@ -26,8 +28,8 @@ ImagePRO/pre_processing/sharpen.py,sha256=y2dzaesjsKyonvobXCPOiFtD1H5y3_Hdi7GpX1
26
28
  ImagePRO/utils/__init__.py,sha256=McVd-o9KtKOpAVYbLi19KxsatPPFFF-Qbt0gdgY3Ln8,170
27
29
  ImagePRO/utils/image.py,sha256=_q34vEcP2-iJdJkiYFbc2obxyK5EPue-pJlrfV50Arc,4458
28
30
  ImagePRO/utils/result.py,sha256=UnOLI8MSJIMCNrvMmhvJgxDXbGAMHmu9hgJ2gw64buQ,6577
29
- imagepro_python-1.5.0.dist-info/licenses/LICENSE,sha256=sehkHN7dbe5umrINZJyJjIf9VTemZSgTfJlq6iIsbJQ,1069
30
- imagepro_python-1.5.0.dist-info/METADATA,sha256=WUGiP3SF3DjQ0-Jl8unZDHx1yBonAdujVl3y_UTT5TE,11063
31
- imagepro_python-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
- imagepro_python-1.5.0.dist-info/top_level.txt,sha256=UOuPzsoWrVKXgUupOSI96shJ6wE8Fhz-8bm8oMlLC3s,9
33
- imagepro_python-1.5.0.dist-info/RECORD,,
31
+ imagepro_python-1.6.0.dist-info/licenses/LICENSE,sha256=sehkHN7dbe5umrINZJyJjIf9VTemZSgTfJlq6iIsbJQ,1069
32
+ imagepro_python-1.6.0.dist-info/METADATA,sha256=WTAor64IzjORRSvenP20bBsJ-y8OevTBtyn0zCy5IgE,11902
33
+ imagepro_python-1.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
34
+ imagepro_python-1.6.0.dist-info/top_level.txt,sha256=UOuPzsoWrVKXgUupOSI96shJ6wE8Fhz-8bm8oMlLC3s,9
35
+ imagepro_python-1.6.0.dist-info/RECORD,,