Coverage for packages/dqm-ml-core/src/dqm_ml_core/utils/image_loading.py: 81%
31 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-21 08:27 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-21 08:27 +0000
1"""Image loading mixin for processors that load images from storage.
3This module provides the ImageLoadingMixin class that handles loading
4images from local filesystem paths or S3, resolving path prefixes
5set by the job for each data selection.
6"""
8from __future__ import annotations
10import io
11import logging
12import os
13from pathlib import Path
14from typing import Any
16from PIL import Image
18logger = logging.getLogger(__name__)
21class ImageLoadingMixin:
22 """Mixin for processors that load images from S3 or local paths.
24 Expects the host class to provide:
25 - ``self.s3_fs``: An S3 filesystem instance (or ``None``).
26 - ``self.current_path_prefix``: A dict mapping column names to path
27 prefixes, set by the job per data selection.
28 """
30 s3_fs: Any = None
32 def _current_image_prefix(self, column: str | None = None) -> str | None:
33 """Return the path prefix for the given column.
35 Reads from ``self.current_path_prefix``, a dict set by the job
36 mapping column names to path prefixes.
38 Args:
39 column: The input column name. Falls back to the first input
40 column if ``None``.
42 Returns:
43 The path prefix string, or ``None``.
44 """
45 prefix_map: dict[str, str] = getattr(self, "current_path_prefix", {})
46 if column is not None:
47 return prefix_map.get(column)
48 cols: list[str] = getattr(self, "input_columns", [])
49 return prefix_map.get(cols[0]) if cols else None
51 def _open_s3_image(self, prefix: str, path: str) -> Image.Image:
52 """Open a PIL Image from S3, reading into memory and copying.
54 Args:
55 prefix: The path prefix within the bucket.
56 path: The relative image path.
58 Returns:
59 A PIL Image object.
60 """
61 bucket = os.getenv("S3_BUCKET_NAME", "")
62 s3_path = f"{bucket}/{prefix}/{path}"
63 with self.s3_fs.open_input_stream(s3_path) as f:
64 img = Image.open(io.BytesIO(f.read())).convert("RGB")
65 return img.copy()
67 def _open_image_from_path(self, path: str, column: str | None = None) -> Image.Image | None:
68 """Open a PIL Image from a string path (S3 or local filesystem).
70 Resolves relative paths by looking up the prefix for the configured
71 input column from ``self.current_path_prefix`` (set by the job for
72 each selection).
74 Args:
75 path: File path or relative path.
76 column: The input column name (used to resolve the path prefix).
78 Returns:
79 A PIL Image object, or ``None`` if the file does not exist.
80 """
81 prefix = self._current_image_prefix(column)
83 if self.s3_fs: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 return self._open_s3_image(prefix or "", path)
86 img_path = Path(prefix) / path if prefix else Path(path)
87 if not img_path.is_file():
88 logger.warning(f"Path does not exist: {img_path}")
89 return None
90 return Image.open(img_path)