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",
]