sysu111's picture
Add PXDepth demo
87608ea
Raw
History Blame Contribute Delete
7.08 kB
"""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",
]