Module facetorch.input
Canonical public image-input contract.
Functions
def canonicalize_image_tensor(tensor: torch.Tensor,
*,
source_kind: str,
input_policy: str = 'coerce',
input_spec: InputSpec | None = None) ‑> CanonicalImage-
Expand source code
def canonicalize_image_tensor( tensor: torch.Tensor, *, source_kind: str, input_policy: str = "coerce", input_spec: Optional[InputSpec] = None, ) -> CanonicalImage: """Convert one image to RGB float32 BCHW ``0..255`` deterministically.""" policy = _normalize_policy(input_policy) if input_spec is not None and not isinstance(input_spec, InputSpec): raise InputError( f"input_spec must be an InputSpec or None, got " f"{type(input_spec).__name__}." ) spec = input_spec or InputSpec() messages: List[str] = [] if not isinstance(tensor, torch.Tensor): raise InputError(f"Expected a Torch tensor, got {type(tensor).__name__}.") if tensor.dtype == torch.bool or tensor.is_complex(): raise InputError(f"Unsupported image dtype {tensor.dtype}.") layout = spec.layout or _default_layout(tensor, source_kind) if spec.layout is None and source_kind == "numpy" and tensor.ndim in {3, 4}: channel_axis = int(tensor.shape[1] if tensor.ndim == 4 else tensor.shape[0]) trailing_axis = int(tensor.shape[-1]) if channel_axis in {1, 3, 4}: channel_first = "BCHW" if tensor.ndim == 4 else "CHW" channel_last = "BHWC" if tensor.ndim == 4 else "HWC" if trailing_axis not in {1, 3, 4}: raise InputError( f"NumPy input shape {tuple(tensor.shape)} looks like " f'{channel_first}; pass InputSpec(layout="{channel_first}") ' "explicitly." ) raise InputError( f"NumPy input shape {tuple(tensor.shape)} is layout-ambiguous; " f'pass InputSpec(layout="{channel_last}") or ' f'InputSpec(layout="{channel_first}") explicitly.' ) if ( spec.layout is None and source_kind == "torch" and tensor.ndim in {3, 4} and int(tensor.shape[-1]) in {1, 3, 4} and int(tensor.shape[1] if tensor.ndim == 4 else tensor.shape[0]) not in {1, 3, 4} ): suggested_layout = "BHWC" if tensor.ndim == 4 else "HWC" raise InputError( f"Torch input shape {tuple(tensor.shape)} looks like {suggested_layout}; " f'pass InputSpec(layout="{suggested_layout}") explicitly.' ) tensor = _to_bchw(tensor, layout) channels = int(tensor.shape[1]) inferred_color = {1: "GRAY", 3: "RGB", 4: "RGBA"}.get(channels) if inferred_color is None: raise InputError( f"Unsupported channel count {channels}; expected 1, 3, or 4 channels." ) color_space = spec.color_space if color_space is None: if policy == "strict" and inferred_color != "RGB": raise InputError( f"Strict mode requires InputSpec.color_space for {inferred_color} input." ) color_space = inferred_color expected_channels = {"GRAY": 1, "RGB": 3, "BGR": 3, "RGBA": 4} if channels != expected_channels[color_space]: raise InputError( f"InputSpec color_space {color_space} requires " f"{expected_channels[color_space]} channels, got {channels}." ) if spec.alpha_mode is not None and color_space != "RGBA": raise InputError("InputSpec.alpha_mode is valid only for RGBA input.") numeric = tensor.detach().clone() # Preserve float64 precision at the range boundary. Other real dtypes use # float32: every integer in the accepted 0..255 interval is represented # exactly, and the conversion also supports unsigned dtypes whose native # reductions are unavailable in PyTorch. bounds = numeric if numeric.dtype == torch.float64 else numeric.to(torch.float32) if not torch.isfinite(bounds).all(): raise InputError("Image values must be finite; NaN and Inf are not supported.") minimum = float(bounds.min().item()) maximum = float(bounds.max().item()) del bounds value_range_declared = spec.value_range is not None value_range = spec.value_range if value_range is None: if policy == "strict": if numeric.dtype != torch.uint8: raise InputError( "Strict tensor/array input defaults to uint8 0..255. " "Declare InputSpec.value_range for any other dtype or range." ) value_range = "0_255" elif numeric.is_floating_point(): if 0.0 <= minimum and maximum <= 1.0: value_range = "0_1" _coercion( messages, policy, "Interpreted floating-point image values as 0..1 and scaled to 0..255.", ) elif 0.0 <= minimum and maximum <= 255.0: value_range = "0_255" _coercion( messages, policy, "Interpreted floating-point image values as 0..255.", ) else: raise InputError( f"Floating-point image range [{minimum}, {maximum}] is unsupported; " "expected 0..1 or 0..255." ) else: value_range = "0_255" if numeric.dtype != torch.uint8: _coercion( messages, policy, f"Converted integer image dtype {numeric.dtype} to float32.", ) if value_range == "0_1": if minimum < 0.0 or maximum > 1.0: raise InputError( f"InputSpec declares 0..1 but observed range [{minimum}, {maximum}]." ) numeric = numeric.to(torch.float32) * 255.0 else: if minimum < 0.0 or maximum > 255.0: if value_range_declared: raise InputError( f"InputSpec declares 0..255 but observed range " f"[{minimum}, {maximum}]." ) raise InputError( f"Integer image range [{minimum}, {maximum}] is unsupported; " "expected 0..255. Declare InputSpec.value_range if the input " "uses a different convention." ) numeric = numeric.to(torch.float32) if color_space == "GRAY": _coercion(messages, policy, "Expanded grayscale input to three RGB channels.") numeric = numeric.repeat(1, 3, 1, 1) elif color_space == "BGR": _coercion(messages, policy, "Converted explicitly declared BGR input to RGB.") numeric = numeric[:, [2, 1, 0], :, :] elif color_space == "RGBA": if policy == "strict" and spec.alpha_mode is None: raise InputError( "Strict RGBA input requires an explicit InputSpec.alpha_mode." ) alpha_mode = spec.alpha_mode or "drop" if alpha_mode != "drop": # guarded by InputSpec; defensive for type checkers raise InputError(f"Unsupported alpha mode {alpha_mode!r}.") _coercion(messages, policy, "Dropped the alpha channel from RGBA input.") numeric = numeric[:, :3, :, :] return CanonicalImage(tensor=numeric.contiguous(), warnings=messages)Convert one image to RGB float32 BCHW
0..255deterministically.
Classes
class InputSpec (layout: Literal['HW', 'CHW', 'HWC', 'BCHW', 'BHWC'] | None = None,
value_range: Literal['0_1', '0_255'] | None = None,
color_space: Literal['GRAY', 'RGB', 'BGR', 'RGBA'] | None = None,
alpha_mode: Literal['drop'] | None = None)-
Expand source code
@dataclass(frozen=True) class InputSpec: """Explicitly describes an array or tensor image representation. Fields may be omitted in ``coerce`` mode. In ``strict`` mode, callers must declare any representation that differs from the source-specific exact defaults: uint8 RGB, CHW for Torch, and HWC for NumPy. NumPy arrays that look channel-first or are plausible under both conventions require an explicit ``CHW``/``BCHW`` or ``HWC``/``BHWC`` layout. """ layout: Optional[InputLayout] = None value_range: Optional[InputValueRange] = None color_space: Optional[InputColorSpace] = None alpha_mode: Optional[AlphaMode] = None def __post_init__(self): normalized = {} for field_name, value, operation in ( ("layout", self.layout, str.upper), ("value_range", self.value_range, str.lower), ("color_space", self.color_space, str.upper), ("alpha_mode", self.alpha_mode, str.lower), ): if value is not None and not isinstance(value, str): raise InputError( f"InputSpec.{field_name} must be a string or None, " f"got {type(value).__name__}." ) normalized[field_name] = operation(value) if value is not None else None for field_name, value in normalized.items(): object.__setattr__(self, field_name, value) valid_values = { "layout": {"HW", "CHW", "HWC", "BCHW", "BHWC"}, "value_range": {"0_1", "0_255"}, "color_space": {"GRAY", "RGB", "BGR", "RGBA"}, "alpha_mode": {"drop"}, } for field_name, allowed in valid_values.items(): value = getattr(self, field_name) if value is not None and value not in allowed: choices = ", ".join(sorted(allowed)) raise InputError( f"Invalid InputSpec.{field_name}={value!r}; expected one of {choices}." )Explicitly describes an array or tensor image representation.
Fields may be omitted in
coercemode. Instrictmode, callers must declare any representation that differs from the source-specific exact defaults: uint8 RGB, CHW for Torch, and HWC for NumPy.NumPy arrays that look channel-first or are plausible under both conventions require an explicit
CHW/BCHWorHWC/BHWClayout.Instance variables
var layout : Literal['HW', 'CHW', 'HWC', 'BCHW', 'BHWC'] | Nonevar value_range : Literal['0_1', '0_255'] | Nonevar color_space : Literal['GRAY', 'RGB', 'BGR', 'RGBA'] | Nonevar alpha_mode : Literal['drop'] | None
class CanonicalImage (tensor: torch.Tensor, warnings: List[str])-
Expand source code
@dataclass class CanonicalImage: """Internal RGB image representation produced at the public boundary.""" tensor: torch.Tensor warnings: List[str]Internal RGB image representation produced at the public boundary.
Instance variables
var tensor : torch.Tensorvar warnings : List[str]