"""I/O for the processed benchmark format used by PXDepth evaluation. Benchmark samples store RGB as JPEG or PNG, depth as a logarithmically encoded 16-bit PNG, optional semantic labels as PNG metadata, and camera information in JSON. The matching writers are shared by the released evaluation-dataset converters so their outputs can be consumed directly by the benchmark loader. """ import io import json import os from pathlib import Path from typing import Any, Dict, IO, List, Optional, Tuple, Union import cv2 import numpy as np from PIL import Image, PngImagePlugin PathOrBinary = Union[str, os.PathLike, IO[bytes]] JsonValue = Union[str, int, float, bool, None, Dict[str, Any], List[Any]] def _read_bytes(path: PathOrBinary) -> bytes: """Read encoded data from a filesystem path or binary stream. Args: path: File path or a binary stream exposing ``read()``. Returns: Encoded file contents as ``bytes``. """ if isinstance(path, (str, os.PathLike)): return Path(path).read_bytes() return path.read() def read_image(path: PathOrBinary) -> np.ndarray: """Decode an RGB image. Args: path: JPEG/PNG path or readable binary stream. Returns: RGB uint8 array with shape ``[H, W, 3]``. Raises: ValueError: If OpenCV cannot decode the input. """ image = cv2.imdecode(np.frombuffer(_read_bytes(path), np.uint8), cv2.IMREAD_COLOR) if image is None: raise ValueError(f"Unable to decode image: {path}") return cv2.cvtColor(image, cv2.COLOR_BGR2RGB) def write_image(path: Union[str, os.PathLike, IO[bytes]], image: np.ndarray, quality: int = 95) -> None: """Encode an RGB image as JPEG. Args: path: Destination path or writable binary stream. image: RGB uint8 array with shape ``[H, W, 3]``. quality: JPEG quality passed to OpenCV. """ encoded = cv2.imencode( ".jpg", cv2.cvtColor(image, cv2.COLOR_RGB2BGR), [cv2.IMWRITE_JPEG_QUALITY, int(quality)], )[1].tobytes() if isinstance(path, (str, os.PathLike)): Path(path).write_bytes(encoded) else: path.write(encoded) def read_depth(path: PathOrBinary) -> np.ndarray: """Decode the logarithmic 16-bit PNG depth representation. Args: path: Encoded depth PNG path or readable binary stream. The PNG must contain ``near`` and ``far`` text metadata. Returns: Float32 depth array ``[H, W]``. Code 0 maps to NaN, code 65535 maps to positive infinity, and codes 1 through 65534 map to finite depth. """ image = Image.open(io.BytesIO(_read_bytes(path))) near = float(image.info["near"]) far = float(image.info["far"]) encoded = np.asarray(image) mask_nan = encoded == 0 mask_inf = encoded == 65535 value = (encoded.astype(np.float32) - 1.0) / 65533.0 depth = near ** (1.0 - value) * far**value if "unit" in image.info: depth *= float(image.info["unit"]) depth[mask_nan] = np.nan depth[mask_inf] = np.inf return depth def write_depth( path: Union[str, os.PathLike, IO[bytes]], depth: np.ndarray, max_range: float = 1e5, compression_level: int = 7, ) -> None: """Encode depth as logarithmic 16-bit PNG with NaN/Inf sentinels. Args: path: Destination path or writable binary stream. depth: Float depth array ``[H, W]``. NaN stores unknown geometry and positive infinity stores known infinite geometry. max_range: Maximum finite ``far / near`` encoding ratio. compression_level: PNG compression level from zero through nine. """ depth = np.asarray(depth, dtype=np.float32) finite = np.isfinite(depth) mask_nan = np.isnan(depth) mask_inf = np.isinf(depth) if not np.any(finite): raise ValueError("Depth encoding requires at least one finite value.") near = max(float(depth[finite].min()), 1e-5) far = max(near * 1.1, min(float(depth[finite].max()), near * float(max_range))) clipped = np.nan_to_num(depth, nan=near, posinf=far, neginf=near).clip(near, far) encoded = 1 + np.round(np.log(clipped / near) / np.log(far / near) * 65533).astype(np.uint16) encoded[mask_nan] = 0 encoded[mask_inf] = 65535 pnginfo = PngImagePlugin.PngInfo() pnginfo.add_text("near", str(near)) pnginfo.add_text("far", str(far)) Image.fromarray(encoded).save(path, pnginfo=pnginfo, compress_level=int(compression_level)) def read_segmentation(path: PathOrBinary) -> Tuple[np.ndarray, Optional[Dict[str, int]]]: """Decode an integer segmentation PNG and its optional label mapping. Args: path: Segmentation PNG path or readable binary stream. Returns: A pair of ``mask`` and ``labels``. ``mask`` has shape ``[H, W]`` and retains the PNG integer dtype. ``labels`` maps names to IDs when the PNG contains label metadata, otherwise it is ``None``. """ image = Image.open(io.BytesIO(_read_bytes(path))) labels = json.loads(image.info["labels"]) if "labels" in image.info else None return np.asarray(image), labels def write_segmentation( path: Union[str, os.PathLike, IO[bytes]], mask: np.ndarray, labels: Optional[Dict[str, int]] = None, compression_level: int = 7, ) -> None: """Write an integer segmentation PNG and optional label mapping. Args: path: Destination path or writable binary stream. mask: Integer label array ``[H, W]`` with uint8 or uint16 dtype. labels: Optional mapping from label names to integer IDs. compression_level: PNG compression level from zero through nine. """ mask = np.asarray(mask) if mask.dtype not in (np.uint8, np.uint16): raise TypeError(f"Segmentation must be uint8 or uint16, got {mask.dtype}.") pnginfo = PngImagePlugin.PngInfo() if labels is not None: pnginfo.add_text("labels", json.dumps(labels, ensure_ascii=True, separators=(",", ":"))) Image.fromarray(mask).save(path, pnginfo=pnginfo, compress_level=int(compression_level)) def read_json(path: Union[str, os.PathLike, IO[str]]) -> JsonValue: """Parse JSON from a path or readable text stream. Args: path: JSON path or text stream exposing ``read()``. Returns: Parsed JSON-compatible Python value. """ text = Path(path).read_text() if isinstance(path, (str, os.PathLike)) else path.read() return json.loads(text) def write_json(path: Union[str, os.PathLike, IO[str]], content: JsonValue) -> None: """Serialize a JSON-compatible value. Args: path: Destination path or writable text stream. content: JSON-compatible scalar, list, or dictionary. """ text = json.dumps(content) if isinstance(path, (str, os.PathLike)): Path(path).write_text(text) else: path.write(text) __all__ = [ "read_depth", "read_image", "read_json", "read_segmentation", "write_depth", "write_image", "write_json", "write_segmentation", ]