Spaces:
Running on Zero
Running on Zero
File size: 7,075 Bytes
87608ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | """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",
]
|